@valentinkolb/filegate 2.4.0 → 2.5.3
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/README.md +109 -512
- 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 -40
- package/LICENSE +0 -21
- package/src/client.ts +0 -436
- package/src/config.ts +0 -41
- package/src/handlers/files.ts +0 -696
- package/src/handlers/indexHandler.ts +0 -107
- package/src/handlers/search.ts +0 -144
- package/src/handlers/thumbnail.ts +0 -174
- package/src/handlers/upload.ts +0 -401
- package/src/index.ts +0 -131
- package/src/lib/index.ts +0 -325
- package/src/lib/openapi.ts +0 -48
- package/src/lib/ownership.ts +0 -133
- package/src/lib/path.ts +0 -128
- package/src/lib/response.ts +0 -10
- package/src/lib/scanner.ts +0 -121
- package/src/lib/validator.ts +0 -21
- package/src/schemas.ts +0 -376
- package/src/utils.ts +0 -282
package/src/lib/scanner.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { readdir, stat } from "node:fs/promises";
|
|
2
|
-
import { join, relative } from "node:path";
|
|
3
|
-
import { config } from "../config";
|
|
4
|
-
import { validatePath } from "./path";
|
|
5
|
-
import {
|
|
6
|
-
indexFile,
|
|
7
|
-
getScanState,
|
|
8
|
-
setScanState,
|
|
9
|
-
touchIndexedAtUnderDir,
|
|
10
|
-
removeStaleEntries,
|
|
11
|
-
} from "./index";
|
|
12
|
-
|
|
13
|
-
export type ScanResult = {
|
|
14
|
-
scanned: number;
|
|
15
|
-
skipped: number;
|
|
16
|
-
added: number;
|
|
17
|
-
moved: number;
|
|
18
|
-
removed: number;
|
|
19
|
-
durationMs: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const emptyResult = (): ScanResult => ({
|
|
23
|
-
scanned: 0,
|
|
24
|
-
skipped: 0,
|
|
25
|
-
added: 0,
|
|
26
|
-
moved: 0,
|
|
27
|
-
removed: 0,
|
|
28
|
-
durationMs: 0,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
export const scanBasePath = async (basePath: string): Promise<ScanResult> => {
|
|
32
|
-
const start = performance.now();
|
|
33
|
-
const scanStart = Date.now();
|
|
34
|
-
const result = emptyResult();
|
|
35
|
-
|
|
36
|
-
const queue: string[] = [basePath];
|
|
37
|
-
const concurrency = Math.max(1, config.indexScanConcurrency);
|
|
38
|
-
|
|
39
|
-
const processDir = async (dirPath: string) => {
|
|
40
|
-
const dirStat = await stat(dirPath).catch(() => null);
|
|
41
|
-
if (!dirStat || !dirStat.isDirectory()) return;
|
|
42
|
-
|
|
43
|
-
const relDir = relative(basePath, dirPath);
|
|
44
|
-
const scanState = await getScanState(basePath, relDir);
|
|
45
|
-
|
|
46
|
-
if (scanState && scanState.mtimeMs === dirStat.mtimeMs) {
|
|
47
|
-
result.skipped++;
|
|
48
|
-
await touchIndexedAtUnderDir(basePath, relDir, scanStart);
|
|
49
|
-
await setScanState(basePath, relDir, dirStat.mtimeMs, scanStart);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
result.scanned++;
|
|
54
|
-
|
|
55
|
-
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
56
|
-
for (const entry of entries) {
|
|
57
|
-
const entryPath = join(dirPath, entry.name);
|
|
58
|
-
const entryStat = await stat(entryPath).catch(() => null);
|
|
59
|
-
if (!entryStat) continue;
|
|
60
|
-
|
|
61
|
-
const relPath = relative(basePath, entryPath);
|
|
62
|
-
const outcome = await indexFile(
|
|
63
|
-
basePath,
|
|
64
|
-
relPath,
|
|
65
|
-
{
|
|
66
|
-
dev: entryStat.dev,
|
|
67
|
-
ino: entryStat.ino,
|
|
68
|
-
size: entryStat.size,
|
|
69
|
-
mtimeMs: entryStat.mtimeMs,
|
|
70
|
-
isDirectory: entryStat.isDirectory(),
|
|
71
|
-
},
|
|
72
|
-
scanStart,
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
if (outcome.action === "added") result.added++;
|
|
76
|
-
if (outcome.action === "moved") result.moved++;
|
|
77
|
-
|
|
78
|
-
if (entry.isDirectory()) {
|
|
79
|
-
queue.push(entryPath);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
await setScanState(basePath, relDir, dirStat.mtimeMs, scanStart);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const workers = Array.from({ length: concurrency }, async () => {
|
|
87
|
-
while (true) {
|
|
88
|
-
const dir = queue.shift();
|
|
89
|
-
if (!dir) break;
|
|
90
|
-
await processDir(dir);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
await Promise.all(workers);
|
|
95
|
-
|
|
96
|
-
result.removed = await removeStaleEntries(basePath, scanStart);
|
|
97
|
-
result.durationMs = Math.round(performance.now() - start);
|
|
98
|
-
|
|
99
|
-
return result;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
export const scanAll = async (): Promise<ScanResult> => {
|
|
103
|
-
const start = performance.now();
|
|
104
|
-
const aggregate = emptyResult();
|
|
105
|
-
|
|
106
|
-
for (const base of config.allowedPaths) {
|
|
107
|
-
const baseResult = await validatePath(base, { allowBasePath: true });
|
|
108
|
-
if (!baseResult.ok) {
|
|
109
|
-
throw new Error(`invalid base path: ${baseResult.error}`);
|
|
110
|
-
}
|
|
111
|
-
const result = await scanBasePath(baseResult.basePath);
|
|
112
|
-
aggregate.scanned += result.scanned;
|
|
113
|
-
aggregate.skipped += result.skipped;
|
|
114
|
-
aggregate.added += result.added;
|
|
115
|
-
aggregate.moved += result.moved;
|
|
116
|
-
aggregate.removed += result.removed;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
aggregate.durationMs = Math.round(performance.now() - start);
|
|
120
|
-
return aggregate;
|
|
121
|
-
};
|
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,376 +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
|
-
fileId: z.string().optional().describe("Stable file identifier (UUID v7)"),
|
|
25
|
-
})
|
|
26
|
-
.describe("Information about a file or directory");
|
|
27
|
-
|
|
28
|
-
export const DirInfoSchema = FileInfoSchema.extend({
|
|
29
|
-
items: z.array(FileInfoSchema).describe("List of files and directories in this directory"),
|
|
30
|
-
total: z.number().describe("Total number of items in the directory"),
|
|
31
|
-
}).describe("Directory information including its contents");
|
|
32
|
-
|
|
33
|
-
// ============================================================================
|
|
34
|
-
// Query Params
|
|
35
|
-
// ============================================================================
|
|
36
|
-
|
|
37
|
-
export const PathQuerySchema = z
|
|
38
|
-
.object({
|
|
39
|
-
path: z.string().min(1).optional().describe("Absolute path to the file or directory"),
|
|
40
|
-
id: z.string().min(1).optional().describe("Stable file identifier (UUID v7)"),
|
|
41
|
-
})
|
|
42
|
-
.refine((v) => (v.path ? !v.id : !!v.id), {
|
|
43
|
-
message: "exactly one of 'path' or 'id' must be provided",
|
|
44
|
-
})
|
|
45
|
-
.describe("Query parameters for path-based operations");
|
|
46
|
-
|
|
47
|
-
export const ContentQuerySchema = z
|
|
48
|
-
.object({
|
|
49
|
-
path: z.string().min(1).optional().describe("Absolute path to the file or directory to download"),
|
|
50
|
-
id: z.string().min(1).optional().describe("Stable file identifier (UUID v7)"),
|
|
51
|
-
inline: z
|
|
52
|
-
.string()
|
|
53
|
-
.optional()
|
|
54
|
-
.transform((v) => v === "true")
|
|
55
|
-
.describe("If 'true', display in browser instead of downloading (Content-Disposition: inline)"),
|
|
56
|
-
})
|
|
57
|
-
.refine((v) => (v.path ? !v.id : !!v.id), {
|
|
58
|
-
message: "exactly one of 'path' or 'id' must be provided",
|
|
59
|
-
})
|
|
60
|
-
.describe("Query parameters for content download");
|
|
61
|
-
|
|
62
|
-
export const InfoQuerySchema = z
|
|
63
|
-
.object({
|
|
64
|
-
path: z.string().min(1).optional().describe("Absolute path to the file or directory"),
|
|
65
|
-
id: z.string().min(1).optional().describe("Stable file identifier (UUID v7)"),
|
|
66
|
-
showHidden: z
|
|
67
|
-
.string()
|
|
68
|
-
.optional()
|
|
69
|
-
.transform((v) => v === "true")
|
|
70
|
-
.describe("If 'true', include hidden files (starting with dot) in directory listings"),
|
|
71
|
-
computeSizes: z
|
|
72
|
-
.string()
|
|
73
|
-
.optional()
|
|
74
|
-
.transform((v) => v === "true")
|
|
75
|
-
.describe("If 'true', compute recursive sizes for directories (slower, default: false)"),
|
|
76
|
-
})
|
|
77
|
-
.refine((v) => (v.path ? !v.id : !!v.id), {
|
|
78
|
-
message: "exactly one of 'path' or 'id' must be provided",
|
|
79
|
-
})
|
|
80
|
-
.describe("Query parameters for file/directory info");
|
|
81
|
-
|
|
82
|
-
export const SearchQuerySchema = z
|
|
83
|
-
.object({
|
|
84
|
-
paths: z.string().min(1).describe("Comma-separated list of base paths to search in"),
|
|
85
|
-
pattern: z.string().min(1).max(500).describe("Glob pattern to match files (e.g., '*.txt', '**/*.pdf')"),
|
|
86
|
-
showHidden: z
|
|
87
|
-
.string()
|
|
88
|
-
.optional()
|
|
89
|
-
.transform((v) => v === "true")
|
|
90
|
-
.describe("If 'true', include hidden files in search results"),
|
|
91
|
-
limit: z
|
|
92
|
-
.string()
|
|
93
|
-
.optional()
|
|
94
|
-
.transform((v) => (v ? parseInt(v, 10) : undefined))
|
|
95
|
-
.describe("Maximum number of results to return"),
|
|
96
|
-
files: z
|
|
97
|
-
.string()
|
|
98
|
-
.optional()
|
|
99
|
-
.transform((v) => v !== "false")
|
|
100
|
-
.describe("If 'false', exclude files from results (default: true)"),
|
|
101
|
-
directories: z
|
|
102
|
-
.string()
|
|
103
|
-
.optional()
|
|
104
|
-
.transform((v) => v === "true")
|
|
105
|
-
.describe("If 'true', include directories in results (default: false)"),
|
|
106
|
-
})
|
|
107
|
-
.describe("Query parameters for glob-based file search");
|
|
108
|
-
|
|
109
|
-
/** Count recursive wildcards (**) in a glob pattern */
|
|
110
|
-
export const countRecursiveWildcards = (pattern: string): number => {
|
|
111
|
-
return (pattern.match(/\*\*/g) || []).length;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// ============================================================================
|
|
115
|
-
// Request Bodies
|
|
116
|
-
// ============================================================================
|
|
117
|
-
|
|
118
|
-
export const MkdirBodySchema = z
|
|
119
|
-
.object({
|
|
120
|
-
path: z.string().min(1).describe("Absolute path of the directory to create"),
|
|
121
|
-
ownerUid: z.number().int().optional().describe("Unix user ID to set as owner"),
|
|
122
|
-
ownerGid: z.number().int().optional().describe("Unix group ID to set as owner"),
|
|
123
|
-
mode: z
|
|
124
|
-
.string()
|
|
125
|
-
.regex(/^[0-7]{3,4}$/)
|
|
126
|
-
.optional()
|
|
127
|
-
.describe("Unix permission mode (e.g., '755' or '0755')"),
|
|
128
|
-
})
|
|
129
|
-
.describe("Request body for creating a directory");
|
|
130
|
-
|
|
131
|
-
export const TransferModeSchema = z
|
|
132
|
-
.enum(["move", "copy"])
|
|
133
|
-
.describe("Transfer operation type: 'move' (rename) or 'copy' (duplicate)");
|
|
134
|
-
|
|
135
|
-
export const TransferBodySchema = z
|
|
136
|
-
.object({
|
|
137
|
-
from: z.string().min(1).describe("Source path of the file or directory"),
|
|
138
|
-
to: z.string().min(1).describe("Destination path for the file or directory"),
|
|
139
|
-
mode: TransferModeSchema,
|
|
140
|
-
ensureUniqueName: z
|
|
141
|
-
.boolean()
|
|
142
|
-
.default(true)
|
|
143
|
-
.describe("If true, append -01, -02, etc. to avoid overwriting existing files (default: true)"),
|
|
144
|
-
ownerUid: z.number().int().optional().describe("Unix user ID for ownership (required for cross-base copy)"),
|
|
145
|
-
ownerGid: z.number().int().optional().describe("Unix group ID for ownership (required for cross-base copy)"),
|
|
146
|
-
fileMode: z
|
|
147
|
-
.string()
|
|
148
|
-
.regex(/^[0-7]{3,4}$/)
|
|
149
|
-
.optional()
|
|
150
|
-
.describe("Unix permission mode for files (e.g., '644', required for cross-base copy)"),
|
|
151
|
-
dirMode: z
|
|
152
|
-
.string()
|
|
153
|
-
.regex(/^[0-7]{3,4}$/)
|
|
154
|
-
.optional()
|
|
155
|
-
.describe("Unix permission mode for directories (e.g., '755', defaults to fileMode if not set)"),
|
|
156
|
-
})
|
|
157
|
-
.describe("Request body for moving or copying files/directories");
|
|
158
|
-
|
|
159
|
-
export const UploadStartBodySchema = z
|
|
160
|
-
.object({
|
|
161
|
-
path: z.string().min(1).describe("Directory path where the file will be uploaded"),
|
|
162
|
-
filename: z.string().min(1).describe("Name of the file to upload"),
|
|
163
|
-
size: z.number().int().positive().describe("Total size of the file in bytes"),
|
|
164
|
-
checksum: z
|
|
165
|
-
.string()
|
|
166
|
-
.regex(/^sha256:[a-f0-9]{64}$/)
|
|
167
|
-
.describe("SHA-256 checksum of the entire file (format: 'sha256:<64 hex chars>')"),
|
|
168
|
-
chunkSize: z.number().int().positive().describe("Size of each chunk in bytes"),
|
|
169
|
-
ownerUid: z.number().int().optional().describe("Unix user ID to set as owner"),
|
|
170
|
-
ownerGid: z.number().int().optional().describe("Unix group ID to set as owner"),
|
|
171
|
-
mode: z
|
|
172
|
-
.string()
|
|
173
|
-
.regex(/^[0-7]{3,4}$/)
|
|
174
|
-
.optional()
|
|
175
|
-
.describe("Unix permission mode for the uploaded file (e.g., '644')"),
|
|
176
|
-
dirMode: z
|
|
177
|
-
.string()
|
|
178
|
-
.regex(/^[0-7]{3,4}$/)
|
|
179
|
-
.optional()
|
|
180
|
-
.describe("Unix permission mode for auto-created parent directories (e.g., '755')"),
|
|
181
|
-
})
|
|
182
|
-
.describe("Request body to start or resume a chunked upload");
|
|
183
|
-
|
|
184
|
-
// ============================================================================
|
|
185
|
-
// Response Schemas
|
|
186
|
-
// ============================================================================
|
|
187
|
-
|
|
188
|
-
export const SearchResultSchema = z
|
|
189
|
-
.object({
|
|
190
|
-
basePath: z.string().describe("Base path that was searched"),
|
|
191
|
-
files: z.array(FileInfoSchema).describe("List of matching files and directories"),
|
|
192
|
-
total: z.number().describe("Number of matches found in this base path"),
|
|
193
|
-
hasMore: z.boolean().describe("True if there are more results beyond the limit"),
|
|
194
|
-
})
|
|
195
|
-
.describe("Search results for a single base path");
|
|
196
|
-
|
|
197
|
-
export const SearchResponseSchema = z
|
|
198
|
-
.object({
|
|
199
|
-
results: z.array(SearchResultSchema).describe("Search results grouped by base path"),
|
|
200
|
-
totalFiles: z.number().describe("Total number of matches across all base paths"),
|
|
201
|
-
})
|
|
202
|
-
.describe("Complete search response with results from all searched paths");
|
|
203
|
-
|
|
204
|
-
export const RescanResponseSchema = z
|
|
205
|
-
.object({
|
|
206
|
-
scanned: z.number().describe("Directories scanned"),
|
|
207
|
-
skipped: z.number().describe("Directories skipped (mtime unchanged)"),
|
|
208
|
-
added: z.number().describe("New files indexed"),
|
|
209
|
-
moved: z.number().describe("Moves detected"),
|
|
210
|
-
removed: z.number().describe("Stale index entries removed"),
|
|
211
|
-
durationMs: z.number().describe("Total scan duration in milliseconds"),
|
|
212
|
-
})
|
|
213
|
-
.describe("Index rescan result");
|
|
214
|
-
|
|
215
|
-
export const IndexStatsSchema = z
|
|
216
|
-
.object({
|
|
217
|
-
totalFiles: z.number().describe("Number of indexed files"),
|
|
218
|
-
totalDirs: z.number().describe("Number of indexed directories"),
|
|
219
|
-
dbSizeBytes: z.number().describe("Database size in bytes (0 if unavailable)"),
|
|
220
|
-
lastScanAt: z.number().nullable().describe("Last scan timestamp (ms since epoch)"),
|
|
221
|
-
})
|
|
222
|
-
.describe("Index statistics");
|
|
223
|
-
|
|
224
|
-
export const BulkResolveBodySchema = z
|
|
225
|
-
.object({
|
|
226
|
-
ids: z.array(z.string().min(1)).describe("List of file IDs to resolve"),
|
|
227
|
-
})
|
|
228
|
-
.describe("Bulk resolve request");
|
|
229
|
-
|
|
230
|
-
export const BulkResolveResponseSchema = z
|
|
231
|
-
.record(z.string(), z.string().nullable())
|
|
232
|
-
.describe("Map of file ID to absolute path or null if missing");
|
|
233
|
-
|
|
234
|
-
export const UploadStartResponseSchema = z
|
|
235
|
-
.object({
|
|
236
|
-
uploadId: z
|
|
237
|
-
.string()
|
|
238
|
-
.regex(/^[a-f0-9]{16}$/)
|
|
239
|
-
.describe("Unique identifier for this upload session"),
|
|
240
|
-
totalChunks: z.number().describe("Total number of chunks expected"),
|
|
241
|
-
chunkSize: z.number().describe("Size of each chunk in bytes"),
|
|
242
|
-
uploadedChunks: z.array(z.number()).describe("Indices of chunks already uploaded (for resume)"),
|
|
243
|
-
completed: z.literal(false).describe("Always false for start response"),
|
|
244
|
-
})
|
|
245
|
-
.describe("Response when starting or resuming a chunked upload");
|
|
246
|
-
|
|
247
|
-
export const UploadChunkProgressSchema = z
|
|
248
|
-
.object({
|
|
249
|
-
chunkIndex: z.number().describe("Index of the chunk that was just uploaded"),
|
|
250
|
-
uploadedChunks: z.array(z.number()).describe("All chunk indices uploaded so far"),
|
|
251
|
-
completed: z.literal(false).describe("False while upload is still in progress"),
|
|
252
|
-
})
|
|
253
|
-
.describe("Response after uploading a chunk (upload not yet complete)");
|
|
254
|
-
|
|
255
|
-
export const UploadChunkCompleteSchema = z
|
|
256
|
-
.object({
|
|
257
|
-
completed: z.literal(true).describe("True when all chunks have been uploaded"),
|
|
258
|
-
file: FileInfoSchema.extend({
|
|
259
|
-
checksum: z.string().describe("SHA-256 checksum of the assembled file"),
|
|
260
|
-
}).describe("Information about the completed file"),
|
|
261
|
-
})
|
|
262
|
-
.describe("Response after uploading the final chunk");
|
|
263
|
-
|
|
264
|
-
export const UploadChunkResponseSchema = z
|
|
265
|
-
.union([UploadChunkProgressSchema, UploadChunkCompleteSchema])
|
|
266
|
-
.describe("Response after uploading a chunk (either progress or completion)");
|
|
267
|
-
|
|
268
|
-
// ============================================================================
|
|
269
|
-
// Header Schemas
|
|
270
|
-
// ============================================================================
|
|
271
|
-
|
|
272
|
-
export const UploadFileHeadersSchema = z
|
|
273
|
-
.object({
|
|
274
|
-
"x-file-path": z.string().min(1).describe("Directory path where the file will be uploaded"),
|
|
275
|
-
"x-file-name": z.string().min(1).describe("Name of the file to upload"),
|
|
276
|
-
"x-owner-uid": z.string().regex(/^\d+$/).transform(Number).optional().describe("Unix user ID to set as owner"),
|
|
277
|
-
"x-owner-gid": z.string().regex(/^\d+$/).transform(Number).optional().describe("Unix group ID to set as owner"),
|
|
278
|
-
"x-file-mode": z
|
|
279
|
-
.string()
|
|
280
|
-
.regex(/^[0-7]{3,4}$/)
|
|
281
|
-
.optional()
|
|
282
|
-
.describe("Unix permission mode for the file (e.g., '644')"),
|
|
283
|
-
"x-dir-mode": z
|
|
284
|
-
.string()
|
|
285
|
-
.regex(/^[0-7]{3,4}$/)
|
|
286
|
-
.optional()
|
|
287
|
-
.describe("Unix permission mode for auto-created directories (e.g., '755')"),
|
|
288
|
-
})
|
|
289
|
-
.describe("Headers for simple file upload");
|
|
290
|
-
|
|
291
|
-
export const UploadChunkHeadersSchema = z
|
|
292
|
-
.object({
|
|
293
|
-
"x-upload-id": z
|
|
294
|
-
.string()
|
|
295
|
-
.regex(/^[a-f0-9]{16}$/)
|
|
296
|
-
.describe("Upload session ID from the start response"),
|
|
297
|
-
"x-chunk-index": z.string().regex(/^\d+$/).transform(Number).describe("Zero-based index of this chunk"),
|
|
298
|
-
"x-chunk-checksum": z
|
|
299
|
-
.string()
|
|
300
|
-
.regex(/^sha256:[a-f0-9]{64}$/)
|
|
301
|
-
.optional()
|
|
302
|
-
.describe("SHA-256 checksum of this chunk for verification (format: 'sha256:<64 hex chars>')"),
|
|
303
|
-
})
|
|
304
|
-
.describe("Headers for uploading a chunk");
|
|
305
|
-
|
|
306
|
-
// ============================================================================
|
|
307
|
-
// Thumbnail Schemas
|
|
308
|
-
// ============================================================================
|
|
309
|
-
|
|
310
|
-
export const ThumbnailFitSchema = z
|
|
311
|
-
.enum(["cover", "contain", "fill", "inside", "outside"])
|
|
312
|
-
.describe(
|
|
313
|
-
"Scaling mode: cover (crop to fill), contain (fit within), fill (stretch), inside (fit, never upscale), outside (cover, never downscale)",
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
export const ThumbnailPositionSchema = z
|
|
317
|
-
.enum(["center", "top", "bottom", "left", "right", "entropy", "attention"])
|
|
318
|
-
.describe("Crop position for 'cover' fit: cardinal directions, or 'entropy'/'attention' for smart cropping");
|
|
319
|
-
|
|
320
|
-
export const ThumbnailFormatSchema = z.enum(["webp", "jpeg", "png", "avif"]).describe("Output image format");
|
|
321
|
-
|
|
322
|
-
export const ImageThumbnailQuerySchema = z
|
|
323
|
-
.object({
|
|
324
|
-
path: z.string().min(1).optional().describe("Absolute path to the image file"),
|
|
325
|
-
id: z.string().min(1).optional().describe("Stable file identifier (UUID v7)"),
|
|
326
|
-
width: z
|
|
327
|
-
.string()
|
|
328
|
-
.optional()
|
|
329
|
-
.transform((v) => (v ? parseInt(v, 10) : 200))
|
|
330
|
-
.refine((v) => v >= 1 && v <= 2000, "width must be between 1 and 2000")
|
|
331
|
-
.describe("Thumbnail width in pixels (default: 200, max: 2000)"),
|
|
332
|
-
height: z
|
|
333
|
-
.string()
|
|
334
|
-
.optional()
|
|
335
|
-
.transform((v) => (v ? parseInt(v, 10) : 200))
|
|
336
|
-
.refine((v) => v >= 1 && v <= 2000, "height must be between 1 and 2000")
|
|
337
|
-
.describe("Thumbnail height in pixels (default: 200, max: 2000)"),
|
|
338
|
-
fit: z
|
|
339
|
-
.string()
|
|
340
|
-
.optional()
|
|
341
|
-
.transform((v) => (v as z.infer<typeof ThumbnailFitSchema>) || "cover")
|
|
342
|
-
.describe("Scaling mode (default: cover)"),
|
|
343
|
-
position: z
|
|
344
|
-
.string()
|
|
345
|
-
.optional()
|
|
346
|
-
.transform((v) => (v as z.infer<typeof ThumbnailPositionSchema>) || "center")
|
|
347
|
-
.describe("Crop position for cover fit (default: center)"),
|
|
348
|
-
format: z
|
|
349
|
-
.string()
|
|
350
|
-
.optional()
|
|
351
|
-
.transform((v) => (v as z.infer<typeof ThumbnailFormatSchema>) || "webp")
|
|
352
|
-
.describe("Output format (default: webp)"),
|
|
353
|
-
quality: z
|
|
354
|
-
.string()
|
|
355
|
-
.optional()
|
|
356
|
-
.transform((v) => (v ? parseInt(v, 10) : 80))
|
|
357
|
-
.refine((v) => v >= 1 && v <= 100, "quality must be between 1 and 100")
|
|
358
|
-
.describe("Output quality 1-100 (default: 80)"),
|
|
359
|
-
})
|
|
360
|
-
.refine((v) => (v.path ? !v.id : !!v.id), {
|
|
361
|
-
message: "exactly one of 'path' or 'id' must be provided",
|
|
362
|
-
})
|
|
363
|
-
.describe("Query parameters for image thumbnail generation");
|
|
364
|
-
|
|
365
|
-
export type ImageThumbnailQuery = z.infer<typeof ImageThumbnailQuerySchema>;
|
|
366
|
-
|
|
367
|
-
// ============================================================================
|
|
368
|
-
// Types
|
|
369
|
-
// ============================================================================
|
|
370
|
-
|
|
371
|
-
export type FileInfo = z.infer<typeof FileInfoSchema>;
|
|
372
|
-
export type DirInfo = z.infer<typeof DirInfoSchema>;
|
|
373
|
-
export type SearchResult = z.infer<typeof SearchResultSchema>;
|
|
374
|
-
export type UploadStartBody = z.infer<typeof UploadStartBodySchema>;
|
|
375
|
-
export type UploadFileHeaders = z.infer<typeof UploadFileHeadersSchema>;
|
|
376
|
-
export type UploadChunkHeaders = z.infer<typeof UploadChunkHeadersSchema>;
|