2024-08-30 16:17:48 +02:00
|
|
|
// Response types
|
|
|
|
|
// ==============
|
2024-08-30 15:16:01 +02:00
|
|
|
|
|
|
|
|
export type GenericResponse = {
|
|
|
|
|
value: string,
|
|
|
|
|
message: string,
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-13 23:20:42 +02:00
|
|
|
export class FSPath {
|
|
|
|
|
path: Array<FSNode>
|
|
|
|
|
base_index: number
|
|
|
|
|
children: Array<FSNode>
|
|
|
|
|
permissions: FSPermissions
|
|
|
|
|
context: FSContext
|
2024-08-30 16:17:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-13 23:20:42 +02:00
|
|
|
export const path_is_shared = (path: FSNode[]): boolean => {
|
|
|
|
|
for (let i = 0; i < path.length; i++) {
|
|
|
|
|
if (path[i].is_shared()) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class FSNode {
|
2025-09-24 15:37:57 +02:00
|
|
|
type: string
|
|
|
|
|
path: string
|
|
|
|
|
name: string
|
|
|
|
|
created: string
|
|
|
|
|
modified: string
|
|
|
|
|
mode_string: string
|
|
|
|
|
mode_octal: string
|
|
|
|
|
created_by: string
|
|
|
|
|
|
|
|
|
|
abuse_type?: string
|
|
|
|
|
abuse_report_time?: string
|
|
|
|
|
|
|
|
|
|
custom_domain_name?: string
|
|
|
|
|
|
|
|
|
|
file_size: number
|
|
|
|
|
file_type: string
|
|
|
|
|
sha256_sum: string
|
|
|
|
|
|
|
|
|
|
id?: string
|
|
|
|
|
properties?: FSNodeProperties
|
|
|
|
|
link_permissions?: FSPermissions
|
|
|
|
|
user_permissions?: { [index: string]: FSPermissions }
|
|
|
|
|
password_permissions?: { [index: string]: FSPermissions }
|
2025-03-28 14:16:20 +01:00
|
|
|
|
|
|
|
|
// Added by us
|
|
|
|
|
|
|
|
|
|
// Indicates whether the file is selected in the file manager
|
2025-10-13 23:20:42 +02:00
|
|
|
fm_selected?: boolean = $state(false)
|
2025-09-24 15:37:57 +02:00
|
|
|
|
2025-10-13 23:20:42 +02:00
|
|
|
is_shared = (): boolean => {
|
|
|
|
|
return (this.link_permissions !== undefined && this.link_permissions.read === true) ||
|
|
|
|
|
(this.user_permissions !== undefined && Object.keys(this.user_permissions).length > 0) ||
|
|
|
|
|
(this.password_permissions !== undefined && Object.keys(this.password_permissions).length > 0)
|
2025-10-10 00:12:14 +02:00
|
|
|
}
|
2025-10-14 00:03:48 +02:00
|
|
|
|
|
|
|
|
download = () => {
|
|
|
|
|
const a = document.createElement("a")
|
|
|
|
|
|
|
|
|
|
if (this.type === "file") {
|
|
|
|
|
a.href = fs_path_url(this.path) + "?attach"
|
|
|
|
|
a.download = this.name
|
|
|
|
|
} else if (this.type === "dir") {
|
|
|
|
|
a.href = fs_path_url(this.path) + "?bulk_download"
|
|
|
|
|
a.download = this.name + ".zip"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// You can't call .click() on an element that is not in the DOM. But
|
|
|
|
|
// emitting a click event works
|
|
|
|
|
a.dispatchEvent(new MouseEvent("click"))
|
|
|
|
|
}
|
2025-10-10 00:12:14 +02:00
|
|
|
}
|
2025-03-28 14:16:20 +01:00
|
|
|
|
|
|
|
|
export type FSNodeProperties = {
|
|
|
|
|
branding_enabled?: string,
|
|
|
|
|
brand_input_color?: string,
|
|
|
|
|
brand_highlight_color?: string,
|
|
|
|
|
brand_danger_color?: string,
|
|
|
|
|
brand_background_color?: string,
|
|
|
|
|
brand_body_color?: string,
|
|
|
|
|
brand_card_color?: string,
|
|
|
|
|
brand_header_image?: string,
|
|
|
|
|
brand_header_link?: string,
|
|
|
|
|
brand_footer_image?: string,
|
|
|
|
|
brand_footer_link?: string,
|
|
|
|
|
brand_background_image?: string,
|
2024-08-30 16:17:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type FSPermissions = {
|
2024-11-19 15:31:51 +01:00
|
|
|
owner: boolean,
|
2024-08-30 16:17:48 +02:00
|
|
|
read: boolean,
|
2024-11-19 15:31:51 +01:00
|
|
|
write: boolean,
|
2024-08-30 16:17:48 +02:00
|
|
|
delete: boolean,
|
2024-09-05 17:28:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type FSContext = {
|
|
|
|
|
premium_transfer: boolean,
|
2024-08-30 15:16:01 +02:00
|
|
|
}
|
|
|
|
|
|
2024-08-30 16:17:48 +02:00
|
|
|
// API parameters
|
|
|
|
|
// ==============
|
|
|
|
|
|
2025-05-14 11:56:52 +02:00
|
|
|
// NodeOptions are options which can be applied by sending a PUT request to a
|
|
|
|
|
// filesystem node. This includes all values which can be set in
|
|
|
|
|
// FSNode.properties
|
2024-08-30 15:16:01 +02:00
|
|
|
export type NodeOptions = {
|
2025-03-28 14:16:20 +01:00
|
|
|
mode?: number,
|
|
|
|
|
created?: string,
|
|
|
|
|
modified?: string,
|
2024-08-30 15:16:01 +02:00
|
|
|
|
2024-11-19 15:31:51 +01:00
|
|
|
// Permissions
|
2025-03-28 14:16:20 +01:00
|
|
|
link_permissions?: FSPermissions,
|
|
|
|
|
user_permissions?: { [index: string]: FSPermissions },
|
|
|
|
|
password_permissions?: { [index: string]: FSPermissions },
|
2025-05-14 11:56:52 +02:00
|
|
|
|
|
|
|
|
// Custom domain name options
|
|
|
|
|
custom_domain_name?: string,
|
|
|
|
|
custom_domain_cert?: string,
|
|
|
|
|
custom_domain_key?: string,
|
2025-03-28 14:16:20 +01:00
|
|
|
} & FSNodeProperties
|
2024-08-30 15:16:01 +02:00
|
|
|
|
2024-08-30 16:17:48 +02:00
|
|
|
// API methods
|
|
|
|
|
// ===========
|
|
|
|
|
|
2024-08-30 15:16:01 +02:00
|
|
|
// mkdir only supports the "mode" option
|
2025-03-28 14:16:20 +01:00
|
|
|
export const fs_mkdir = async (path: string, opts?: NodeOptions) => {
|
2024-08-30 15:16:01 +02:00
|
|
|
const form = new FormData()
|
|
|
|
|
form.append("action", "mkdir")
|
|
|
|
|
|
2025-03-28 14:16:20 +01:00
|
|
|
if (opts !== undefined && opts.mode !== undefined) {
|
2024-08-30 15:16:01 +02:00
|
|
|
form.append("mode", opts.mode.toFixed(0))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await fs_check_response(
|
|
|
|
|
await fetch(fs_path_url(path), { method: "POST", body: form })
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_mkdirall = async (path: string, opts: NodeOptions) => {
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
form.append("action", "mkdirall")
|
|
|
|
|
|
|
|
|
|
if (opts && opts.mode) {
|
|
|
|
|
form.append("mode", opts.mode.toFixed(0))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await fs_check_response(
|
|
|
|
|
await fetch(fs_path_url(path), { method: "POST", body: form })
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_get_node = async (path: string) => {
|
2025-10-13 23:20:42 +02:00
|
|
|
const resp = await fs_check_response(
|
2024-08-30 15:16:01 +02:00
|
|
|
await fetch(fs_path_url(path) + "?stat")
|
|
|
|
|
) as FSPath
|
2025-10-13 23:20:42 +02:00
|
|
|
|
|
|
|
|
const fsp = new FSPath()
|
|
|
|
|
fsp.path = new Array(resp.path.length)
|
|
|
|
|
resp.path.forEach((node, index) => {
|
|
|
|
|
fsp.path[index] = Object.assign(new FSNode(), node)
|
|
|
|
|
})
|
|
|
|
|
fsp.base_index = resp.base_index
|
|
|
|
|
fsp.children = new Array(resp.children.length)
|
|
|
|
|
resp.children.forEach((node, index) => {
|
|
|
|
|
fsp.children[index] = Object.assign(new FSNode(), node)
|
|
|
|
|
})
|
|
|
|
|
fsp.permissions = resp.permissions
|
|
|
|
|
fsp.context = resp.context
|
|
|
|
|
return fsp
|
2024-08-30 15:16:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Updates a node's parameters. Available options are:
|
|
|
|
|
// - created, Date object
|
|
|
|
|
// - modified, Date object
|
|
|
|
|
// - mode, file mode formatted as octal string
|
|
|
|
|
// - shared, boolean. If true the node will receive a public ID
|
|
|
|
|
//
|
|
|
|
|
// Returns the modified filesystem node object
|
|
|
|
|
export const fs_update = async (path: string, opts: NodeOptions) => {
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
form.append("action", "update")
|
|
|
|
|
|
|
|
|
|
for (let key of Object.keys(opts)) {
|
2024-11-19 15:31:51 +01:00
|
|
|
if (opts[key] === undefined) {
|
|
|
|
|
continue
|
|
|
|
|
} else if ((key === "created" || key === "modified")) {
|
2025-01-09 23:43:31 +01:00
|
|
|
form.append(key, new Date(opts[key]).toISOString())
|
2024-11-19 15:31:51 +01:00
|
|
|
} else if (typeof opts[key] === "object") {
|
|
|
|
|
form.append(key, JSON.stringify(opts[key]))
|
2024-08-30 15:16:01 +02:00
|
|
|
} else {
|
|
|
|
|
form.append(key, opts[key])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-13 23:20:42 +02:00
|
|
|
const resp = await fs_check_response(
|
2024-08-30 15:16:01 +02:00
|
|
|
await fetch(fs_path_url(path), { method: "POST", body: form })
|
2025-10-13 23:20:42 +02:00
|
|
|
)
|
|
|
|
|
return Object.assign(new FSNode(), resp)
|
2024-08-30 15:16:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_rename = async (old_path: string, new_path: string) => {
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
form.append("action", "rename")
|
|
|
|
|
form.append("target", new_path)
|
|
|
|
|
|
2025-10-13 23:20:42 +02:00
|
|
|
const resp = await fs_check_response(
|
2024-08-30 15:16:01 +02:00
|
|
|
await fetch(fs_path_url(old_path), { method: "POST", body: form })
|
2025-10-13 23:20:42 +02:00
|
|
|
)
|
|
|
|
|
return Object.assign(new FSNode(), resp)
|
2024-08-30 15:16:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_delete = async (path: string) => {
|
|
|
|
|
return await fs_check_response(
|
|
|
|
|
await fetch(fs_path_url(path), { method: "DELETE" })
|
|
|
|
|
) as GenericResponse
|
|
|
|
|
}
|
2024-09-12 15:11:50 +02:00
|
|
|
|
2024-08-30 15:16:01 +02:00
|
|
|
export const fs_delete_all = async (path: string) => {
|
|
|
|
|
return await fs_check_response(
|
|
|
|
|
await fetch(fs_path_url(path) + "?recursive", { method: "DELETE" })
|
|
|
|
|
) as GenericResponse
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_search = async (path: string, term: string, limit = 10) => {
|
|
|
|
|
return await fs_check_response(
|
|
|
|
|
await fetch(
|
|
|
|
|
fs_path_url(path) +
|
|
|
|
|
"?search=" + encodeURIComponent(term) +
|
|
|
|
|
"&limit=" + limit
|
|
|
|
|
)
|
2025-03-28 14:16:20 +01:00
|
|
|
) as string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type TimeSeries = {
|
|
|
|
|
timestamps: string[],
|
|
|
|
|
amounts: number[],
|
|
|
|
|
}
|
|
|
|
|
export type NodeTimeSeries = {
|
|
|
|
|
downloads: TimeSeries,
|
|
|
|
|
transfer_free: TimeSeries,
|
|
|
|
|
transfer_paid: TimeSeries,
|
2024-08-30 15:16:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_timeseries = async (path: string, start: Date, end: Date, interval = 60) => {
|
|
|
|
|
return await fs_check_response(
|
|
|
|
|
await fetch(
|
|
|
|
|
fs_path_url(path) +
|
|
|
|
|
"?timeseries" +
|
|
|
|
|
"&start=" + start.toISOString() +
|
|
|
|
|
"&end=" + end.toISOString() +
|
|
|
|
|
"&interval=" + interval
|
|
|
|
|
)
|
2025-03-28 14:16:20 +01:00
|
|
|
) as NodeTimeSeries
|
2024-08-30 15:16:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_import = async (parent_dir_path = "", filelist: Array<string>) => {
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
form.append("action", "import")
|
|
|
|
|
form.append("files", JSON.stringify(filelist))
|
|
|
|
|
|
|
|
|
|
return await fs_check_response(
|
|
|
|
|
await fetch(fs_path_url(parent_dir_path), { method: "POST", body: form })
|
|
|
|
|
) as GenericResponse
|
|
|
|
|
}
|
2024-08-30 16:17:48 +02:00
|
|
|
|
|
|
|
|
// Utility functions
|
|
|
|
|
// =================
|
|
|
|
|
|
|
|
|
|
export const fs_check_response = async (resp: Response) => {
|
|
|
|
|
let text = await resp.text()
|
|
|
|
|
if (resp.status >= 400) {
|
|
|
|
|
let error: any
|
|
|
|
|
try {
|
|
|
|
|
error = JSON.parse(text) as GenericResponse
|
|
|
|
|
} catch (err) {
|
|
|
|
|
error = text
|
|
|
|
|
}
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
return JSON.parse(text)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_path_url = (path: string) => {
|
|
|
|
|
if (!path || path.length === 0) {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if (path[0] !== "/") {
|
|
|
|
|
path = "/" + path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (window["api_endpoint"] !== undefined) {
|
|
|
|
|
return window["api_endpoint"] + "/filesystem" + fs_encode_path(path)
|
|
|
|
|
} else {
|
2025-01-09 23:43:31 +01:00
|
|
|
throw Error("fs_path_url: api_endpoint is undefined")
|
2024-08-30 16:17:48 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_encode_path = (path: string) => {
|
|
|
|
|
// Encode all path elements separately to preserve forward slashes
|
|
|
|
|
let split = path.split("/")
|
|
|
|
|
for (let i = 0; i < split.length; i++) {
|
|
|
|
|
split[i] = encodeURIComponent(split[i])
|
|
|
|
|
}
|
|
|
|
|
return split.join("/")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_split_path = (path: string) => {
|
|
|
|
|
let patharr = path.split("/")
|
|
|
|
|
return { base: patharr.pop(), parent: patharr.join("/") }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_node_type = (node: FSNode) => {
|
|
|
|
|
if (node.type === "dir") {
|
|
|
|
|
return "dir"
|
|
|
|
|
} else if (node.file_type === "application/bittorrent" || node.file_type === "application/x-bittorrent") {
|
|
|
|
|
return "torrent"
|
2024-11-14 22:35:34 +01:00
|
|
|
} else if (
|
|
|
|
|
node.file_type === "application/zip" ||
|
2024-12-04 15:06:58 +01:00
|
|
|
node.file_type === "application/x-7z-compressed" ||
|
|
|
|
|
node.file_type === "application/x-tar" ||
|
|
|
|
|
(node.file_type === "application/gzip" && node.name.endsWith(".tar.gz")) ||
|
|
|
|
|
(node.file_type === "application/x-xz" && node.name.endsWith(".tar.xz")) ||
|
|
|
|
|
(node.file_type === "application/zstd" && node.name.endsWith(".tar.zst"))
|
2024-11-14 22:35:34 +01:00
|
|
|
) {
|
2024-08-30 16:17:48 +02:00
|
|
|
return "zip"
|
|
|
|
|
} else if (node.file_type.startsWith("image")) {
|
|
|
|
|
return "image"
|
|
|
|
|
} else if (
|
|
|
|
|
node.file_type.startsWith("video") ||
|
|
|
|
|
node.file_type === "application/matroska" ||
|
|
|
|
|
node.file_type === "application/x-matroska"
|
|
|
|
|
) {
|
|
|
|
|
return "video"
|
|
|
|
|
} else if (
|
|
|
|
|
node.file_type.startsWith("audio") ||
|
|
|
|
|
node.file_type === "application/ogg" ||
|
|
|
|
|
node.name.endsWith(".mp3")
|
|
|
|
|
) {
|
|
|
|
|
return "audio"
|
|
|
|
|
} else if (
|
|
|
|
|
node.file_type === "application/pdf" ||
|
|
|
|
|
node.file_type === "application/x-pdf"
|
|
|
|
|
) {
|
|
|
|
|
return "pdf"
|
|
|
|
|
} else if (
|
|
|
|
|
node.file_type === "application/json" ||
|
2024-11-14 17:56:01 +01:00
|
|
|
node.file_type === "application/x-yaml" ||
|
|
|
|
|
node.file_type === "application/x-shellscript" ||
|
2024-08-30 16:17:48 +02:00
|
|
|
node.file_type.startsWith("text")
|
|
|
|
|
) {
|
|
|
|
|
return "text"
|
|
|
|
|
} else {
|
|
|
|
|
return "file"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_node_icon = (node: FSNode, width = 64, height = 64) => {
|
|
|
|
|
if (node.type === "dir") {
|
|
|
|
|
// Folders with an ID are publically shared, use the shared folder icon
|
2025-10-13 23:20:42 +02:00
|
|
|
if (node.is_shared()) {
|
2024-08-30 16:17:48 +02:00
|
|
|
return "/res/img/mime/folder-remote.png"
|
|
|
|
|
} else {
|
|
|
|
|
return "/res/img/mime/folder.png"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-09 23:43:31 +01:00
|
|
|
return fs_thumbnail_url(node.path, width, height) + "&mod=" + new Date(node.modified).getTime()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fs_thumbnail_url = (path: string, width = 64, height = 64) => {
|
|
|
|
|
return fs_path_url(path) + "?thumbnail&width=" + width + "&height=" + height
|
2024-08-30 16:17:48 +02:00
|
|
|
}
|
2025-03-28 14:16:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
export const fs_share_url = (path: FSNode[]): string => {
|
|
|
|
|
let share_path = fs_share_path(path)
|
|
|
|
|
if (share_path !== "") {
|
|
|
|
|
share_path = window.location.protocol + "//" + window.location.host + "/d/" + fs_encode_path(share_path)
|
|
|
|
|
}
|
|
|
|
|
return share_path
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-05 21:29:07 +02:00
|
|
|
|
|
|
|
|
export const fs_share_hotlink_url = (path: FSNode[]): string => {
|
|
|
|
|
let share_path = fs_share_path(path)
|
|
|
|
|
if (share_path !== "") {
|
|
|
|
|
share_path = window.location.protocol + "//" + window.location.host + fs_path_url(share_path)
|
|
|
|
|
}
|
|
|
|
|
return share_path
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 14:16:20 +01:00
|
|
|
export const fs_share_path = (path: FSNode[]): string => {
|
|
|
|
|
let share_url = ""
|
|
|
|
|
let bucket_idx = -1
|
|
|
|
|
|
|
|
|
|
// Find the last node in the path that has a public ID
|
|
|
|
|
for (let i = path.length - 1; i >= 0; i--) {
|
2025-10-13 23:20:42 +02:00
|
|
|
if (path[i].is_shared()) {
|
2025-03-28 14:16:20 +01:00
|
|
|
bucket_idx = i
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (bucket_idx !== -1) {
|
|
|
|
|
share_url = path[bucket_idx].id
|
|
|
|
|
|
|
|
|
|
// Construct the path starting from the bucket
|
|
|
|
|
for (let i = bucket_idx + 1; i < path.length; i++) {
|
|
|
|
|
share_url += "/" + path[i].name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return share_url
|
|
|
|
|
}
|