@valentinkolb/filegate 2.3.6 → 2.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activity.d.ts +15 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +21 -0
- package/dist/activity.js.map +1 -0
- package/dist/capabilities.d.ts +9 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +11 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/client.d.ts +37 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +77 -0
- package/dist/client.js.map +1 -0
- package/dist/core.d.ts +26 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +58 -0
- package/dist/core.js.map +1 -0
- package/dist/downloads.d.ts +9 -0
- package/dist/downloads.d.ts.map +1 -0
- package/dist/downloads.js +11 -0
- package/dist/downloads.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/errors.js.map +1 -0
- package/dist/index-client.d.ts +17 -0
- package/dist/index-client.d.ts.map +1 -0
- package/dist/index-client.js +29 -0
- package/dist/index-client.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes.d.ts +27 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +71 -0
- package/dist/nodes.js.map +1 -0
- package/dist/paths.d.ts +45 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +71 -0
- package/dist/paths.js.map +1 -0
- package/dist/search.d.ts +17 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +25 -0
- package/dist/search.js.map +1 -0
- package/dist/stats.d.ts +9 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +11 -0
- package/dist/stats.js.map +1 -0
- package/dist/transfers.d.ts +9 -0
- package/dist/transfers.d.ts.map +1 -0
- package/dist/transfers.js +14 -0
- package/dist/transfers.js.map +1 -0
- package/dist/types.d.ts +285 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/uploads.d.ts +246 -0
- package/dist/uploads.d.ts.map +1 -0
- package/dist/uploads.js +580 -0
- package/dist/uploads.js.map +1 -0
- package/dist/utils.d.ts +25 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +41 -0
- package/dist/utils.js.map +1 -0
- package/dist/versions.d.ts +76 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/versions.js +82 -0
- package/dist/versions.js.map +1 -0
- package/package.json +36 -41
- package/LICENSE +0 -21
- package/README.md +0 -569
- package/src/client.ts +0 -436
- package/src/config.ts +0 -37
- package/src/handlers/files.ts +0 -513
- package/src/handlers/search.ts +0 -137
- package/src/handlers/thumbnail.ts +0 -148
- package/src/handlers/upload.ts +0 -379
- package/src/index.ts +0 -94
- package/src/lib/openapi.ts +0 -47
- package/src/lib/ownership.ts +0 -133
- package/src/lib/path.ts +0 -128
- package/src/lib/response.ts +0 -10
- package/src/lib/validator.ts +0 -21
- package/src/schemas.ts +0 -329
- package/src/utils.ts +0 -282
package/src/lib/ownership.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { chown, chmod, readdir, stat } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
import { config } from "../config";
|
|
5
|
-
|
|
6
|
-
export type Ownership = {
|
|
7
|
-
uid: number;
|
|
8
|
-
gid: number;
|
|
9
|
-
mode: number; // octal, e.g. 0o600
|
|
10
|
-
dirMode?: number; // octal, optional - if not set, derived from mode
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
// Derive directory mode from file mode (add x where r is set)
|
|
14
|
-
// 644 → 755, 600 → 700, 664 → 775
|
|
15
|
-
export const fileModeToDirectoryMode = (fileMode: number): number => {
|
|
16
|
-
let dirMode = fileMode;
|
|
17
|
-
if (fileMode & 0o400) dirMode |= 0o100; // owner read → owner exec
|
|
18
|
-
if (fileMode & 0o040) dirMode |= 0o010; // group read → group exec
|
|
19
|
-
if (fileMode & 0o004) dirMode |= 0o001; // other read → other exec
|
|
20
|
-
return dirMode;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// Get effective directory mode (explicit or derived from file mode)
|
|
24
|
-
export const getEffectiveDirMode = (ownership: Ownership): number => {
|
|
25
|
-
return ownership.dirMode ?? fileModeToDirectoryMode(ownership.mode);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const parseOwnershipHeaders = (req: Request): Ownership | null => {
|
|
29
|
-
const uid = req.headers.get("X-Owner-UID");
|
|
30
|
-
const gid = req.headers.get("X-Owner-GID");
|
|
31
|
-
const mode = req.headers.get("X-File-Mode");
|
|
32
|
-
const dirMode = req.headers.get("X-Dir-Mode");
|
|
33
|
-
|
|
34
|
-
if (!uid || !gid || !mode) return null;
|
|
35
|
-
|
|
36
|
-
const parsedMode = parseInt(mode, 8);
|
|
37
|
-
if (isNaN(parsedMode)) return null;
|
|
38
|
-
|
|
39
|
-
const parsedDirMode = dirMode ? parseInt(dirMode, 8) : undefined;
|
|
40
|
-
if (dirMode && isNaN(parsedDirMode!)) return null;
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
uid: parseInt(uid, 10),
|
|
44
|
-
gid: parseInt(gid, 10),
|
|
45
|
-
mode: parsedMode,
|
|
46
|
-
dirMode: parsedDirMode,
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const parseOwnershipBody = (body: {
|
|
51
|
-
ownerUid?: number;
|
|
52
|
-
ownerGid?: number;
|
|
53
|
-
mode?: string;
|
|
54
|
-
dirMode?: string;
|
|
55
|
-
}): Ownership | null => {
|
|
56
|
-
if (body.ownerUid == null || body.ownerGid == null || !body.mode) return null;
|
|
57
|
-
|
|
58
|
-
const mode = parseInt(body.mode, 8);
|
|
59
|
-
if (isNaN(mode)) return null;
|
|
60
|
-
|
|
61
|
-
const dirMode = body.dirMode ? parseInt(body.dirMode, 8) : undefined;
|
|
62
|
-
if (body.dirMode && isNaN(dirMode!)) return null;
|
|
63
|
-
|
|
64
|
-
return { uid: body.ownerUid, gid: body.ownerGid, mode, dirMode };
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export const applyOwnership = async (path: string, ownership: Ownership | null): Promise<string | null> => {
|
|
68
|
-
if (!ownership) return null;
|
|
69
|
-
|
|
70
|
-
const uid = config.devUid ?? ownership.uid;
|
|
71
|
-
const gid = config.devGid ?? ownership.gid;
|
|
72
|
-
|
|
73
|
-
if (config.isDev) {
|
|
74
|
-
console.log(
|
|
75
|
-
`[DEV] chown ${path}: ${ownership.uid}->${uid}, ${ownership.gid}->${gid}, mode=${ownership.mode.toString(8)}`,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
await chown(path, uid, gid);
|
|
81
|
-
await chmod(path, ownership.mode);
|
|
82
|
-
return null;
|
|
83
|
-
} catch (e: any) {
|
|
84
|
-
if (e.code === "EPERM") return "permission denied (not root?)";
|
|
85
|
-
if (e.code === "EINVAL") return `invalid uid=${uid} or gid=${gid}`;
|
|
86
|
-
return `ownership failed: ${e.message}`;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Apply ownership to directory chain from targetDir up to (not including) basePath
|
|
91
|
-
export const applyOwnershipToNewDirs = async (
|
|
92
|
-
targetDir: string,
|
|
93
|
-
basePath: string,
|
|
94
|
-
ownership: Ownership,
|
|
95
|
-
): Promise<void> => {
|
|
96
|
-
const dirMode = getEffectiveDirMode(ownership);
|
|
97
|
-
const dirOwnership: Ownership = { ...ownership, mode: dirMode };
|
|
98
|
-
|
|
99
|
-
let current = targetDir;
|
|
100
|
-
while (current !== basePath && current.startsWith(basePath + "/")) {
|
|
101
|
-
await applyOwnership(current, dirOwnership);
|
|
102
|
-
current = dirname(current);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// Apply ownership recursively to a file or directory (and all its contents)
|
|
107
|
-
export const applyOwnershipRecursive = async (path: string, ownership: Ownership | null): Promise<string | null> => {
|
|
108
|
-
if (!ownership) return null;
|
|
109
|
-
|
|
110
|
-
const s = await stat(path);
|
|
111
|
-
|
|
112
|
-
if (s.isDirectory()) {
|
|
113
|
-
// Apply directory mode to the directory itself
|
|
114
|
-
const dirMode = getEffectiveDirMode(ownership);
|
|
115
|
-
const dirOwnership: Ownership = { ...ownership, mode: dirMode };
|
|
116
|
-
const err = await applyOwnership(path, dirOwnership);
|
|
117
|
-
if (err) return err;
|
|
118
|
-
|
|
119
|
-
// Recursively apply to contents
|
|
120
|
-
const entries = await readdir(path, { withFileTypes: true });
|
|
121
|
-
for (const entry of entries) {
|
|
122
|
-
const fullPath = join(path, entry.name);
|
|
123
|
-
const err = await applyOwnershipRecursive(fullPath, ownership);
|
|
124
|
-
if (err) return err;
|
|
125
|
-
}
|
|
126
|
-
} else {
|
|
127
|
-
// Apply file mode to files
|
|
128
|
-
const err = await applyOwnership(path, ownership);
|
|
129
|
-
if (err) return err;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return null;
|
|
133
|
-
};
|
package/src/lib/path.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { realpath, mkdir } from "node:fs/promises";
|
|
2
|
-
import { join, normalize, dirname, basename } from "node:path";
|
|
3
|
-
import { config } from "../config";
|
|
4
|
-
import { applyOwnershipToNewDirs, type Ownership } from "./ownership";
|
|
5
|
-
|
|
6
|
-
export type PathResult =
|
|
7
|
-
| { ok: true; realPath: string; basePath: string }
|
|
8
|
-
| { ok: false; error: string; status: 400 | 403 | 404 };
|
|
9
|
-
|
|
10
|
-
// Cache for resolved base paths (they don't change at runtime)
|
|
11
|
-
const realBaseCache = new Map<string, string>();
|
|
12
|
-
|
|
13
|
-
const getRealBase = async (basePath: string): Promise<string | null> => {
|
|
14
|
-
const cached = realBaseCache.get(basePath);
|
|
15
|
-
if (cached) return cached;
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const realBase = await realpath(basePath);
|
|
19
|
-
realBaseCache.set(basePath, realBase);
|
|
20
|
-
return realBase;
|
|
21
|
-
} catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type ValidatePathOptions = {
|
|
27
|
-
/** If true, allows operating on the base path itself */
|
|
28
|
-
allowBasePath?: boolean;
|
|
29
|
-
/** If true, creates parent directories if they don't exist */
|
|
30
|
-
createParents?: boolean;
|
|
31
|
-
/** Ownership to apply to newly created directories */
|
|
32
|
-
ownership?: Ownership | null;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Validates path is within allowed base paths, resolves symlinks.
|
|
37
|
-
* Optionally creates parent directories with ownership.
|
|
38
|
-
*/
|
|
39
|
-
export const validatePath = async (path: string, options: ValidatePathOptions = {}): Promise<PathResult> => {
|
|
40
|
-
const { allowBasePath = false, createParents = false, ownership = null } = options;
|
|
41
|
-
const cleaned = normalize(path);
|
|
42
|
-
|
|
43
|
-
// Find matching base
|
|
44
|
-
let basePath: string | null = null;
|
|
45
|
-
for (const base of config.allowedPaths) {
|
|
46
|
-
const cleanBase = normalize(base);
|
|
47
|
-
if (cleaned === cleanBase) {
|
|
48
|
-
if (!allowBasePath) {
|
|
49
|
-
return { ok: false, error: "cannot operate on base path", status: 403 };
|
|
50
|
-
}
|
|
51
|
-
basePath = cleanBase;
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
if (cleaned.startsWith(cleanBase + "/")) {
|
|
55
|
-
basePath = cleanBase;
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!basePath) {
|
|
61
|
-
return { ok: false, error: "path not allowed", status: 403 };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Create parent directories if requested (AFTER base validation)
|
|
65
|
-
if (createParents) {
|
|
66
|
-
const parentPath = dirname(cleaned);
|
|
67
|
-
await mkdir(parentPath, { recursive: true });
|
|
68
|
-
|
|
69
|
-
// Apply ownership to newly created directories
|
|
70
|
-
if (ownership) {
|
|
71
|
-
const realBase = await getRealBase(basePath);
|
|
72
|
-
if (realBase) {
|
|
73
|
-
await applyOwnershipToNewDirs(parentPath, realBase, ownership);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Resolve symlinks
|
|
79
|
-
let realPath: string;
|
|
80
|
-
try {
|
|
81
|
-
realPath = await realpath(cleaned);
|
|
82
|
-
} catch (e: any) {
|
|
83
|
-
if (e.code === "ENOENT") {
|
|
84
|
-
// Path doesn't exist yet - validate parent
|
|
85
|
-
try {
|
|
86
|
-
const parentReal = await realpath(dirname(cleaned));
|
|
87
|
-
realPath = join(parentReal, basename(cleaned));
|
|
88
|
-
} catch {
|
|
89
|
-
return { ok: false, error: "parent path not found", status: 404 };
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
return { ok: false, error: "path resolution failed", status: 400 };
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Verify still within base after symlink resolution (cached)
|
|
97
|
-
const realBase = await getRealBase(basePath);
|
|
98
|
-
if (!realBase) {
|
|
99
|
-
return { ok: false, error: "base path invalid", status: 500 as any };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (realPath !== realBase && !realPath.startsWith(realBase + "/")) {
|
|
103
|
-
return { ok: false, error: "symlink escape not allowed", status: 403 };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return { ok: true, realPath, basePath: realBase };
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
export type SameBaseResult =
|
|
110
|
-
| { ok: true; realPath: string; basePath: string; realTo: string }
|
|
111
|
-
| { ok: false; error: string; status: 400 | 403 | 404 };
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Validates both paths are in the same base (for move/copy).
|
|
115
|
-
*/
|
|
116
|
-
export const validateSameBase = async (from: string, to: string): Promise<SameBaseResult> => {
|
|
117
|
-
const fromResult = await validatePath(from);
|
|
118
|
-
if (!fromResult.ok) return fromResult;
|
|
119
|
-
|
|
120
|
-
const toResult = await validatePath(to);
|
|
121
|
-
if (!toResult.ok) return toResult;
|
|
122
|
-
|
|
123
|
-
if (fromResult.basePath !== toResult.basePath) {
|
|
124
|
-
return { ok: false, error: "cross-basepath not allowed", status: 400 };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { ...fromResult, realTo: toResult.realPath };
|
|
128
|
-
};
|
package/src/lib/response.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export const json = <T>(data: T, status = 200) =>
|
|
2
|
-
Response.json(data, { status });
|
|
3
|
-
|
|
4
|
-
export const error = (msg: string, status = 400) =>
|
|
5
|
-
Response.json({ error: msg }, { status });
|
|
6
|
-
|
|
7
|
-
export const notFound = (msg = "not found") => error(msg, 404);
|
|
8
|
-
export const forbidden = (msg = "forbidden") => error(msg, 403);
|
|
9
|
-
export const badRequest = (msg: string) => error(msg, 400);
|
|
10
|
-
export const serverError = (msg = "internal error") => error(msg, 500);
|
package/src/lib/validator.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { validator } from "hono-openapi";
|
|
2
|
-
import type { ZodType } from "zod";
|
|
3
|
-
import type { ValidationTargets, Context } from "hono";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Zod validator middleware with OpenAPI support.
|
|
7
|
-
* Returns 400 with error details on validation failure.
|
|
8
|
-
*/
|
|
9
|
-
export const v = <Target extends keyof ValidationTargets, T extends ZodType>(target: Target, schema: T) =>
|
|
10
|
-
validator(target, schema, (result, c: Context) => {
|
|
11
|
-
if (!result.success) {
|
|
12
|
-
const issues = result.error as readonly { message: string; path?: readonly unknown[] }[];
|
|
13
|
-
const msg = issues
|
|
14
|
-
?.map((issue) => {
|
|
15
|
-
const path = issue.path?.map((p) => String(p)).join(".");
|
|
16
|
-
return path ? `${path}: ${issue.message}` : issue.message;
|
|
17
|
-
})
|
|
18
|
-
.join(", ");
|
|
19
|
-
return c.json({ error: msg || "validation failed" }, 400);
|
|
20
|
-
}
|
|
21
|
-
});
|
package/src/schemas.ts
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
// ============================================================================
|
|
4
|
-
// Common
|
|
5
|
-
// ============================================================================
|
|
6
|
-
|
|
7
|
-
export const ErrorSchema = z
|
|
8
|
-
.object({
|
|
9
|
-
error: z.string().describe("Error message describing what went wrong"),
|
|
10
|
-
})
|
|
11
|
-
.describe("Error response returned when a request fails");
|
|
12
|
-
|
|
13
|
-
export const FileTypeSchema = z.enum(["file", "directory"]).describe("Type of filesystem entry");
|
|
14
|
-
|
|
15
|
-
export const FileInfoSchema = z
|
|
16
|
-
.object({
|
|
17
|
-
name: z.string().describe("Filename or directory name"),
|
|
18
|
-
path: z.string().describe("Relative path from the base directory"),
|
|
19
|
-
type: FileTypeSchema,
|
|
20
|
-
size: z.number().describe("File size in bytes, or total directory size for directories"),
|
|
21
|
-
mtime: z.iso.datetime().describe("Last modification time in ISO 8601 format"),
|
|
22
|
-
isHidden: z.boolean().describe("True if the name starts with a dot"),
|
|
23
|
-
mimeType: z.string().optional().describe("MIME type of the file (only for files)"),
|
|
24
|
-
})
|
|
25
|
-
.describe("Information about a file or directory");
|
|
26
|
-
|
|
27
|
-
export const DirInfoSchema = FileInfoSchema.extend({
|
|
28
|
-
items: z.array(FileInfoSchema).describe("List of files and directories in this directory"),
|
|
29
|
-
total: z.number().describe("Total number of items in the directory"),
|
|
30
|
-
}).describe("Directory information including its contents");
|
|
31
|
-
|
|
32
|
-
// ============================================================================
|
|
33
|
-
// Query Params
|
|
34
|
-
// ============================================================================
|
|
35
|
-
|
|
36
|
-
export const PathQuerySchema = z
|
|
37
|
-
.object({
|
|
38
|
-
path: z.string().min(1).describe("Absolute path to the file or directory"),
|
|
39
|
-
})
|
|
40
|
-
.describe("Query parameters for path-based operations");
|
|
41
|
-
|
|
42
|
-
export const ContentQuerySchema = z
|
|
43
|
-
.object({
|
|
44
|
-
path: z.string().min(1).describe("Absolute path to the file or directory to download"),
|
|
45
|
-
inline: z
|
|
46
|
-
.string()
|
|
47
|
-
.optional()
|
|
48
|
-
.transform((v) => v === "true")
|
|
49
|
-
.describe("If 'true', display in browser instead of downloading (Content-Disposition: inline)"),
|
|
50
|
-
})
|
|
51
|
-
.describe("Query parameters for content download");
|
|
52
|
-
|
|
53
|
-
export const InfoQuerySchema = z
|
|
54
|
-
.object({
|
|
55
|
-
path: z.string().min(1).describe("Absolute path to the file or directory"),
|
|
56
|
-
showHidden: z
|
|
57
|
-
.string()
|
|
58
|
-
.optional()
|
|
59
|
-
.transform((v) => v === "true")
|
|
60
|
-
.describe("If 'true', include hidden files (starting with dot) in directory listings"),
|
|
61
|
-
computeSizes: z
|
|
62
|
-
.string()
|
|
63
|
-
.optional()
|
|
64
|
-
.transform((v) => v === "true")
|
|
65
|
-
.describe("If 'true', compute recursive sizes for directories (slower, default: false)"),
|
|
66
|
-
})
|
|
67
|
-
.describe("Query parameters for file/directory info");
|
|
68
|
-
|
|
69
|
-
export const SearchQuerySchema = z
|
|
70
|
-
.object({
|
|
71
|
-
paths: z.string().min(1).describe("Comma-separated list of base paths to search in"),
|
|
72
|
-
pattern: z.string().min(1).max(500).describe("Glob pattern to match files (e.g., '*.txt', '**/*.pdf')"),
|
|
73
|
-
showHidden: z
|
|
74
|
-
.string()
|
|
75
|
-
.optional()
|
|
76
|
-
.transform((v) => v === "true")
|
|
77
|
-
.describe("If 'true', include hidden files in search results"),
|
|
78
|
-
limit: z
|
|
79
|
-
.string()
|
|
80
|
-
.optional()
|
|
81
|
-
.transform((v) => (v ? parseInt(v, 10) : undefined))
|
|
82
|
-
.describe("Maximum number of results to return"),
|
|
83
|
-
files: z
|
|
84
|
-
.string()
|
|
85
|
-
.optional()
|
|
86
|
-
.transform((v) => v !== "false")
|
|
87
|
-
.describe("If 'false', exclude files from results (default: true)"),
|
|
88
|
-
directories: z
|
|
89
|
-
.string()
|
|
90
|
-
.optional()
|
|
91
|
-
.transform((v) => v === "true")
|
|
92
|
-
.describe("If 'true', include directories in results (default: false)"),
|
|
93
|
-
})
|
|
94
|
-
.describe("Query parameters for glob-based file search");
|
|
95
|
-
|
|
96
|
-
/** Count recursive wildcards (**) in a glob pattern */
|
|
97
|
-
export const countRecursiveWildcards = (pattern: string): number => {
|
|
98
|
-
return (pattern.match(/\*\*/g) || []).length;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// ============================================================================
|
|
102
|
-
// Request Bodies
|
|
103
|
-
// ============================================================================
|
|
104
|
-
|
|
105
|
-
export const MkdirBodySchema = z
|
|
106
|
-
.object({
|
|
107
|
-
path: z.string().min(1).describe("Absolute path of the directory to create"),
|
|
108
|
-
ownerUid: z.number().int().optional().describe("Unix user ID to set as owner"),
|
|
109
|
-
ownerGid: z.number().int().optional().describe("Unix group ID to set as owner"),
|
|
110
|
-
mode: z
|
|
111
|
-
.string()
|
|
112
|
-
.regex(/^[0-7]{3,4}$/)
|
|
113
|
-
.optional()
|
|
114
|
-
.describe("Unix permission mode (e.g., '755' or '0755')"),
|
|
115
|
-
})
|
|
116
|
-
.describe("Request body for creating a directory");
|
|
117
|
-
|
|
118
|
-
export const TransferModeSchema = z
|
|
119
|
-
.enum(["move", "copy"])
|
|
120
|
-
.describe("Transfer operation type: 'move' (rename) or 'copy' (duplicate)");
|
|
121
|
-
|
|
122
|
-
export const TransferBodySchema = z
|
|
123
|
-
.object({
|
|
124
|
-
from: z.string().min(1).describe("Source path of the file or directory"),
|
|
125
|
-
to: z.string().min(1).describe("Destination path for the file or directory"),
|
|
126
|
-
mode: TransferModeSchema,
|
|
127
|
-
ensureUniqueName: z
|
|
128
|
-
.boolean()
|
|
129
|
-
.default(true)
|
|
130
|
-
.describe("If true, append -01, -02, etc. to avoid overwriting existing files (default: true)"),
|
|
131
|
-
ownerUid: z.number().int().optional().describe("Unix user ID for ownership (required for cross-base copy)"),
|
|
132
|
-
ownerGid: z.number().int().optional().describe("Unix group ID for ownership (required for cross-base copy)"),
|
|
133
|
-
fileMode: z
|
|
134
|
-
.string()
|
|
135
|
-
.regex(/^[0-7]{3,4}$/)
|
|
136
|
-
.optional()
|
|
137
|
-
.describe("Unix permission mode for files (e.g., '644', required for cross-base copy)"),
|
|
138
|
-
dirMode: z
|
|
139
|
-
.string()
|
|
140
|
-
.regex(/^[0-7]{3,4}$/)
|
|
141
|
-
.optional()
|
|
142
|
-
.describe("Unix permission mode for directories (e.g., '755', defaults to fileMode if not set)"),
|
|
143
|
-
})
|
|
144
|
-
.describe("Request body for moving or copying files/directories");
|
|
145
|
-
|
|
146
|
-
export const UploadStartBodySchema = z
|
|
147
|
-
.object({
|
|
148
|
-
path: z.string().min(1).describe("Directory path where the file will be uploaded"),
|
|
149
|
-
filename: z.string().min(1).describe("Name of the file to upload"),
|
|
150
|
-
size: z.number().int().positive().describe("Total size of the file in bytes"),
|
|
151
|
-
checksum: z
|
|
152
|
-
.string()
|
|
153
|
-
.regex(/^sha256:[a-f0-9]{64}$/)
|
|
154
|
-
.describe("SHA-256 checksum of the entire file (format: 'sha256:<64 hex chars>')"),
|
|
155
|
-
chunkSize: z.number().int().positive().describe("Size of each chunk in bytes"),
|
|
156
|
-
ownerUid: z.number().int().optional().describe("Unix user ID to set as owner"),
|
|
157
|
-
ownerGid: z.number().int().optional().describe("Unix group ID to set as owner"),
|
|
158
|
-
mode: z
|
|
159
|
-
.string()
|
|
160
|
-
.regex(/^[0-7]{3,4}$/)
|
|
161
|
-
.optional()
|
|
162
|
-
.describe("Unix permission mode for the uploaded file (e.g., '644')"),
|
|
163
|
-
dirMode: z
|
|
164
|
-
.string()
|
|
165
|
-
.regex(/^[0-7]{3,4}$/)
|
|
166
|
-
.optional()
|
|
167
|
-
.describe("Unix permission mode for auto-created parent directories (e.g., '755')"),
|
|
168
|
-
})
|
|
169
|
-
.describe("Request body to start or resume a chunked upload");
|
|
170
|
-
|
|
171
|
-
// ============================================================================
|
|
172
|
-
// Response Schemas
|
|
173
|
-
// ============================================================================
|
|
174
|
-
|
|
175
|
-
export const SearchResultSchema = z
|
|
176
|
-
.object({
|
|
177
|
-
basePath: z.string().describe("Base path that was searched"),
|
|
178
|
-
files: z.array(FileInfoSchema).describe("List of matching files and directories"),
|
|
179
|
-
total: z.number().describe("Number of matches found in this base path"),
|
|
180
|
-
hasMore: z.boolean().describe("True if there are more results beyond the limit"),
|
|
181
|
-
})
|
|
182
|
-
.describe("Search results for a single base path");
|
|
183
|
-
|
|
184
|
-
export const SearchResponseSchema = z
|
|
185
|
-
.object({
|
|
186
|
-
results: z.array(SearchResultSchema).describe("Search results grouped by base path"),
|
|
187
|
-
totalFiles: z.number().describe("Total number of matches across all base paths"),
|
|
188
|
-
})
|
|
189
|
-
.describe("Complete search response with results from all searched paths");
|
|
190
|
-
|
|
191
|
-
export const UploadStartResponseSchema = z
|
|
192
|
-
.object({
|
|
193
|
-
uploadId: z
|
|
194
|
-
.string()
|
|
195
|
-
.regex(/^[a-f0-9]{16}$/)
|
|
196
|
-
.describe("Unique identifier for this upload session"),
|
|
197
|
-
totalChunks: z.number().describe("Total number of chunks expected"),
|
|
198
|
-
chunkSize: z.number().describe("Size of each chunk in bytes"),
|
|
199
|
-
uploadedChunks: z.array(z.number()).describe("Indices of chunks already uploaded (for resume)"),
|
|
200
|
-
completed: z.literal(false).describe("Always false for start response"),
|
|
201
|
-
})
|
|
202
|
-
.describe("Response when starting or resuming a chunked upload");
|
|
203
|
-
|
|
204
|
-
export const UploadChunkProgressSchema = z
|
|
205
|
-
.object({
|
|
206
|
-
chunkIndex: z.number().describe("Index of the chunk that was just uploaded"),
|
|
207
|
-
uploadedChunks: z.array(z.number()).describe("All chunk indices uploaded so far"),
|
|
208
|
-
completed: z.literal(false).describe("False while upload is still in progress"),
|
|
209
|
-
})
|
|
210
|
-
.describe("Response after uploading a chunk (upload not yet complete)");
|
|
211
|
-
|
|
212
|
-
export const UploadChunkCompleteSchema = z
|
|
213
|
-
.object({
|
|
214
|
-
completed: z.literal(true).describe("True when all chunks have been uploaded"),
|
|
215
|
-
file: FileInfoSchema.extend({
|
|
216
|
-
checksum: z.string().describe("SHA-256 checksum of the assembled file"),
|
|
217
|
-
}).describe("Information about the completed file"),
|
|
218
|
-
})
|
|
219
|
-
.describe("Response after uploading the final chunk");
|
|
220
|
-
|
|
221
|
-
export const UploadChunkResponseSchema = z
|
|
222
|
-
.union([UploadChunkProgressSchema, UploadChunkCompleteSchema])
|
|
223
|
-
.describe("Response after uploading a chunk (either progress or completion)");
|
|
224
|
-
|
|
225
|
-
// ============================================================================
|
|
226
|
-
// Header Schemas
|
|
227
|
-
// ============================================================================
|
|
228
|
-
|
|
229
|
-
export const UploadFileHeadersSchema = z
|
|
230
|
-
.object({
|
|
231
|
-
"x-file-path": z.string().min(1).describe("Directory path where the file will be uploaded"),
|
|
232
|
-
"x-file-name": z.string().min(1).describe("Name of the file to upload"),
|
|
233
|
-
"x-owner-uid": z.string().regex(/^\d+$/).transform(Number).optional().describe("Unix user ID to set as owner"),
|
|
234
|
-
"x-owner-gid": z.string().regex(/^\d+$/).transform(Number).optional().describe("Unix group ID to set as owner"),
|
|
235
|
-
"x-file-mode": z
|
|
236
|
-
.string()
|
|
237
|
-
.regex(/^[0-7]{3,4}$/)
|
|
238
|
-
.optional()
|
|
239
|
-
.describe("Unix permission mode for the file (e.g., '644')"),
|
|
240
|
-
"x-dir-mode": z
|
|
241
|
-
.string()
|
|
242
|
-
.regex(/^[0-7]{3,4}$/)
|
|
243
|
-
.optional()
|
|
244
|
-
.describe("Unix permission mode for auto-created directories (e.g., '755')"),
|
|
245
|
-
})
|
|
246
|
-
.describe("Headers for simple file upload");
|
|
247
|
-
|
|
248
|
-
export const UploadChunkHeadersSchema = z
|
|
249
|
-
.object({
|
|
250
|
-
"x-upload-id": z
|
|
251
|
-
.string()
|
|
252
|
-
.regex(/^[a-f0-9]{16}$/)
|
|
253
|
-
.describe("Upload session ID from the start response"),
|
|
254
|
-
"x-chunk-index": z.string().regex(/^\d+$/).transform(Number).describe("Zero-based index of this chunk"),
|
|
255
|
-
"x-chunk-checksum": z
|
|
256
|
-
.string()
|
|
257
|
-
.regex(/^sha256:[a-f0-9]{64}$/)
|
|
258
|
-
.optional()
|
|
259
|
-
.describe("SHA-256 checksum of this chunk for verification (format: 'sha256:<64 hex chars>')"),
|
|
260
|
-
})
|
|
261
|
-
.describe("Headers for uploading a chunk");
|
|
262
|
-
|
|
263
|
-
// ============================================================================
|
|
264
|
-
// Thumbnail Schemas
|
|
265
|
-
// ============================================================================
|
|
266
|
-
|
|
267
|
-
export const ThumbnailFitSchema = z
|
|
268
|
-
.enum(["cover", "contain", "fill", "inside", "outside"])
|
|
269
|
-
.describe(
|
|
270
|
-
"Scaling mode: cover (crop to fill), contain (fit within), fill (stretch), inside (fit, never upscale), outside (cover, never downscale)",
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
export const ThumbnailPositionSchema = z
|
|
274
|
-
.enum(["center", "top", "bottom", "left", "right", "entropy", "attention"])
|
|
275
|
-
.describe("Crop position for 'cover' fit: cardinal directions, or 'entropy'/'attention' for smart cropping");
|
|
276
|
-
|
|
277
|
-
export const ThumbnailFormatSchema = z.enum(["webp", "jpeg", "png", "avif"]).describe("Output image format");
|
|
278
|
-
|
|
279
|
-
export const ImageThumbnailQuerySchema = z
|
|
280
|
-
.object({
|
|
281
|
-
path: z.string().min(1).describe("Absolute path to the image file"),
|
|
282
|
-
width: z
|
|
283
|
-
.string()
|
|
284
|
-
.optional()
|
|
285
|
-
.transform((v) => (v ? parseInt(v, 10) : 200))
|
|
286
|
-
.refine((v) => v >= 1 && v <= 2000, "width must be between 1 and 2000")
|
|
287
|
-
.describe("Thumbnail width in pixels (default: 200, max: 2000)"),
|
|
288
|
-
height: z
|
|
289
|
-
.string()
|
|
290
|
-
.optional()
|
|
291
|
-
.transform((v) => (v ? parseInt(v, 10) : 200))
|
|
292
|
-
.refine((v) => v >= 1 && v <= 2000, "height must be between 1 and 2000")
|
|
293
|
-
.describe("Thumbnail height in pixels (default: 200, max: 2000)"),
|
|
294
|
-
fit: z
|
|
295
|
-
.string()
|
|
296
|
-
.optional()
|
|
297
|
-
.transform((v) => (v as z.infer<typeof ThumbnailFitSchema>) || "cover")
|
|
298
|
-
.describe("Scaling mode (default: cover)"),
|
|
299
|
-
position: z
|
|
300
|
-
.string()
|
|
301
|
-
.optional()
|
|
302
|
-
.transform((v) => (v as z.infer<typeof ThumbnailPositionSchema>) || "center")
|
|
303
|
-
.describe("Crop position for cover fit (default: center)"),
|
|
304
|
-
format: z
|
|
305
|
-
.string()
|
|
306
|
-
.optional()
|
|
307
|
-
.transform((v) => (v as z.infer<typeof ThumbnailFormatSchema>) || "webp")
|
|
308
|
-
.describe("Output format (default: webp)"),
|
|
309
|
-
quality: z
|
|
310
|
-
.string()
|
|
311
|
-
.optional()
|
|
312
|
-
.transform((v) => (v ? parseInt(v, 10) : 80))
|
|
313
|
-
.refine((v) => v >= 1 && v <= 100, "quality must be between 1 and 100")
|
|
314
|
-
.describe("Output quality 1-100 (default: 80)"),
|
|
315
|
-
})
|
|
316
|
-
.describe("Query parameters for image thumbnail generation");
|
|
317
|
-
|
|
318
|
-
export type ImageThumbnailQuery = z.infer<typeof ImageThumbnailQuerySchema>;
|
|
319
|
-
|
|
320
|
-
// ============================================================================
|
|
321
|
-
// Types
|
|
322
|
-
// ============================================================================
|
|
323
|
-
|
|
324
|
-
export type FileInfo = z.infer<typeof FileInfoSchema>;
|
|
325
|
-
export type DirInfo = z.infer<typeof DirInfoSchema>;
|
|
326
|
-
export type SearchResult = z.infer<typeof SearchResultSchema>;
|
|
327
|
-
export type UploadStartBody = z.infer<typeof UploadStartBodySchema>;
|
|
328
|
-
export type UploadFileHeaders = z.infer<typeof UploadFileHeadersSchema>;
|
|
329
|
-
export type UploadChunkHeaders = z.infer<typeof UploadChunkHeadersSchema>;
|