@valentinkolb/filegate 0.0.1

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/src/client.ts ADDED
@@ -0,0 +1,354 @@
1
+ import type { z } from "zod";
2
+ import type {
3
+ FileInfoSchema,
4
+ DirInfoSchema,
5
+ SearchResponseSchema,
6
+ UploadStartResponseSchema,
7
+ UploadChunkResponseSchema,
8
+ ErrorSchema,
9
+ } from "./schemas";
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ export type FileInfo = z.infer<typeof FileInfoSchema>;
16
+ export type DirInfo = z.infer<typeof DirInfoSchema>;
17
+ export type SearchResponse = z.infer<typeof SearchResponseSchema>;
18
+ export type UploadStartResponse = z.infer<typeof UploadStartResponseSchema>;
19
+ export type UploadChunkResponse = z.infer<typeof UploadChunkResponseSchema>;
20
+ export type ApiError = z.infer<typeof ErrorSchema>;
21
+
22
+ export type FileProxyResponse<T> = { ok: true; data: T } | { ok: false; error: string; status: number };
23
+
24
+ type Headers = Record<string, string>;
25
+
26
+ export interface ClientOptions {
27
+ url: string;
28
+ token: string;
29
+ fetch?: typeof fetch;
30
+ }
31
+
32
+ // --- Info ---
33
+ export interface InfoOptions {
34
+ path: string;
35
+ showHidden?: boolean;
36
+ }
37
+
38
+ // --- Download ---
39
+ export interface DownloadOptions {
40
+ path: string;
41
+ }
42
+
43
+ // --- Upload Single ---
44
+ export interface UploadSingleOptions {
45
+ path: string;
46
+ filename: string;
47
+ data: Blob | ArrayBuffer | Uint8Array;
48
+ uid?: number;
49
+ gid?: number;
50
+ mode?: string;
51
+ }
52
+
53
+ // --- Upload Chunked Start ---
54
+ export interface UploadChunkedStartOptions {
55
+ path: string;
56
+ filename: string;
57
+ size: number;
58
+ checksum: string;
59
+ chunkSize: number;
60
+ uid?: number;
61
+ gid?: number;
62
+ mode?: string;
63
+ }
64
+
65
+ // --- Upload Chunked Send ---
66
+ export interface UploadChunkedSendOptions {
67
+ uploadId: string;
68
+ index: number;
69
+ data: Blob | ArrayBuffer | Uint8Array;
70
+ checksum?: string;
71
+ }
72
+
73
+ // --- Mkdir ---
74
+ export interface MkdirOptions {
75
+ path: string;
76
+ uid?: number;
77
+ gid?: number;
78
+ mode?: string;
79
+ }
80
+
81
+ // --- Delete ---
82
+ export interface DeleteOptions {
83
+ path: string;
84
+ }
85
+
86
+ // --- Move ---
87
+ export interface MoveOptions {
88
+ from: string;
89
+ to: string;
90
+ }
91
+
92
+ // --- Copy ---
93
+ export interface CopyOptions {
94
+ from: string;
95
+ to: string;
96
+ }
97
+
98
+ // --- Glob (Search) ---
99
+ export interface GlobOptions {
100
+ paths: string[];
101
+ pattern: string;
102
+ showHidden?: boolean;
103
+ limit?: number;
104
+ }
105
+
106
+ // ============================================================================
107
+ // Upload Namespace Class
108
+ // ============================================================================
109
+
110
+ class UploadClient {
111
+ constructor(
112
+ private readonly url: string,
113
+ private readonly hdrs: () => Headers,
114
+ private readonly jsonHdrs: () => Headers,
115
+ private readonly _fetch: typeof fetch,
116
+ private readonly handleResponse: <T>(res: Response) => Promise<FileProxyResponse<T>>,
117
+ ) {}
118
+
119
+ async single(opts: UploadSingleOptions): Promise<FileProxyResponse<FileInfo>> {
120
+ const uploadHdrs: Headers = {
121
+ ...this.hdrs(),
122
+ "X-File-Path": opts.path,
123
+ "X-File-Name": opts.filename,
124
+ };
125
+ if (opts.uid !== undefined) uploadHdrs["X-Owner-UID"] = String(opts.uid);
126
+ if (opts.gid !== undefined) uploadHdrs["X-Owner-GID"] = String(opts.gid);
127
+ if (opts.mode) uploadHdrs["X-File-Mode"] = opts.mode;
128
+
129
+ const res = await this._fetch(`${this.url}/files/content`, {
130
+ method: "PUT",
131
+ headers: uploadHdrs,
132
+ body: opts.data,
133
+ });
134
+ return this.handleResponse(res);
135
+ }
136
+
137
+ readonly chunked = {
138
+ start: async (opts: UploadChunkedStartOptions): Promise<FileProxyResponse<UploadStartResponse>> => {
139
+ const body = {
140
+ path: opts.path,
141
+ filename: opts.filename,
142
+ size: opts.size,
143
+ checksum: opts.checksum,
144
+ chunkSize: opts.chunkSize,
145
+ ownerUid: opts.uid,
146
+ ownerGid: opts.gid,
147
+ mode: opts.mode,
148
+ };
149
+
150
+ const res = await this._fetch(`${this.url}/files/upload/start`, {
151
+ method: "POST",
152
+ headers: this.jsonHdrs(),
153
+ body: JSON.stringify(body),
154
+ });
155
+ return this.handleResponse(res);
156
+ },
157
+
158
+ send: async (opts: UploadChunkedSendOptions): Promise<FileProxyResponse<UploadChunkResponse>> => {
159
+ const headers: Headers = {
160
+ ...this.hdrs(),
161
+ "X-Upload-Id": opts.uploadId,
162
+ "X-Chunk-Index": String(opts.index),
163
+ };
164
+ if (opts.checksum) {
165
+ headers["X-Chunk-Checksum"] = opts.checksum;
166
+ }
167
+
168
+ const res = await this._fetch(`${this.url}/files/upload/chunk`, {
169
+ method: "POST",
170
+ headers,
171
+ body: opts.data,
172
+ });
173
+ return this.handleResponse(res);
174
+ },
175
+ };
176
+ }
177
+
178
+ // ============================================================================
179
+ // Client Class
180
+ // ============================================================================
181
+
182
+ export class Filegate {
183
+ private readonly url: string;
184
+ private readonly token: string;
185
+ private readonly _fetch: typeof fetch;
186
+
187
+ readonly upload: UploadClient;
188
+
189
+ constructor(opts: ClientOptions) {
190
+ this.url = opts.url.replace(/\/$/, "");
191
+ this.token = opts.token;
192
+ this._fetch = opts.fetch ?? fetch;
193
+
194
+ this.upload = new UploadClient(
195
+ this.url,
196
+ () => this.hdrs(),
197
+ () => this.jsonHdrs(),
198
+ this._fetch,
199
+ (res) => this.handleResponse(res),
200
+ );
201
+ }
202
+
203
+ private hdrs(): Headers {
204
+ return { Authorization: `Bearer ${this.token}` };
205
+ }
206
+
207
+ private jsonHdrs(): Headers {
208
+ return { ...this.hdrs(), "Content-Type": "application/json" };
209
+ }
210
+
211
+ private async handleResponse<T>(res: Response): Promise<FileProxyResponse<T>> {
212
+ if (!res.ok) {
213
+ const body = (await res.json().catch(() => ({ error: "unknown error" }))) as ApiError;
214
+ return { ok: false, error: body.error || "unknown error", status: res.status };
215
+ }
216
+ const data = (await res.json()) as T;
217
+ return { ok: true, data };
218
+ }
219
+
220
+ // ==========================================================================
221
+ // Info
222
+ // ==========================================================================
223
+
224
+ async info(opts: InfoOptions): Promise<FileProxyResponse<FileInfo | DirInfo>> {
225
+ const params = new URLSearchParams({
226
+ path: opts.path,
227
+ showHidden: String(opts.showHidden ?? false),
228
+ });
229
+ const res = await this._fetch(`${this.url}/files/info?${params}`, { headers: this.hdrs() });
230
+ return this.handleResponse(res);
231
+ }
232
+
233
+ // ==========================================================================
234
+ // Download
235
+ // ==========================================================================
236
+
237
+ async download(opts: DownloadOptions): Promise<FileProxyResponse<Response>> {
238
+ const params = new URLSearchParams({ path: opts.path });
239
+ const res = await this._fetch(`${this.url}/files/content?${params}`, { headers: this.hdrs() });
240
+ if (!res.ok) {
241
+ const body = (await res.json().catch(() => ({ error: "unknown error" }))) as ApiError;
242
+ return { ok: false, error: body.error || "unknown error", status: res.status };
243
+ }
244
+ return { ok: true, data: res };
245
+ }
246
+
247
+ // ==========================================================================
248
+ // Directory Operations
249
+ // ==========================================================================
250
+
251
+ async mkdir(opts: MkdirOptions): Promise<FileProxyResponse<FileInfo>> {
252
+ const body: Record<string, unknown> = { path: opts.path };
253
+ if (opts.uid !== undefined) body.ownerUid = opts.uid;
254
+ if (opts.gid !== undefined) body.ownerGid = opts.gid;
255
+ if (opts.mode) body.mode = opts.mode;
256
+
257
+ const res = await this._fetch(`${this.url}/files/mkdir`, {
258
+ method: "POST",
259
+ headers: this.jsonHdrs(),
260
+ body: JSON.stringify(body),
261
+ });
262
+ return this.handleResponse(res);
263
+ }
264
+
265
+ // ==========================================================================
266
+ // Delete
267
+ // ==========================================================================
268
+
269
+ async delete(opts: DeleteOptions): Promise<FileProxyResponse<void>> {
270
+ const params = new URLSearchParams({ path: opts.path });
271
+ const res = await this._fetch(`${this.url}/files/delete?${params}`, {
272
+ method: "DELETE",
273
+ headers: this.hdrs(),
274
+ });
275
+ if (!res.ok) {
276
+ const body = (await res.json().catch(() => ({ error: "unknown error" }))) as ApiError;
277
+ return { ok: false, error: body.error || "unknown error", status: res.status };
278
+ }
279
+ return { ok: true, data: undefined };
280
+ }
281
+
282
+ // ==========================================================================
283
+ // Move & Copy
284
+ // ==========================================================================
285
+
286
+ async move(opts: MoveOptions): Promise<FileProxyResponse<FileInfo>> {
287
+ const res = await this._fetch(`${this.url}/files/move`, {
288
+ method: "POST",
289
+ headers: this.jsonHdrs(),
290
+ body: JSON.stringify({ from: opts.from, to: opts.to }),
291
+ });
292
+ return this.handleResponse(res);
293
+ }
294
+
295
+ async copy(opts: CopyOptions): Promise<FileProxyResponse<FileInfo>> {
296
+ const res = await this._fetch(`${this.url}/files/copy`, {
297
+ method: "POST",
298
+ headers: this.jsonHdrs(),
299
+ body: JSON.stringify({ from: opts.from, to: opts.to }),
300
+ });
301
+ return this.handleResponse(res);
302
+ }
303
+
304
+ // ==========================================================================
305
+ // Glob (Search)
306
+ // ==========================================================================
307
+
308
+ async glob(opts: GlobOptions): Promise<FileProxyResponse<SearchResponse>> {
309
+ const params = new URLSearchParams({
310
+ paths: opts.paths.join(","),
311
+ pattern: opts.pattern,
312
+ });
313
+ if (opts.showHidden) params.set("showHidden", "true");
314
+ if (opts.limit) params.set("limit", String(opts.limit));
315
+
316
+ const res = await this._fetch(`${this.url}/files/search?${params}`, { headers: this.hdrs() });
317
+ return this.handleResponse(res);
318
+ }
319
+ }
320
+
321
+ // ============================================================================
322
+ // Default Instance (server-side only)
323
+ // ============================================================================
324
+
325
+ const createDefaultInstance = (): Filegate => {
326
+ const url = process.env.FILEGATE_URL;
327
+ const token = process.env.FILEGATE_TOKEN;
328
+
329
+ if (!url || !token) {
330
+ throw new Error(
331
+ "FILEGATE_URL and FILEGATE_TOKEN environment variables are required.\n" +
332
+ "Either set these variables or create an instance manually:\n\n" +
333
+ ' import { Filegate } from "filegate/client";\n' +
334
+ ' const client = new Filegate({ url: "...", token: "..." });',
335
+ );
336
+ }
337
+
338
+ return new Filegate({ url, token });
339
+ };
340
+
341
+ let _instance: Filegate | null = null;
342
+
343
+ export const filegate: Filegate = new Proxy({} as Filegate, {
344
+ get(_target, prop) {
345
+ if (_instance === null) {
346
+ _instance = createDefaultInstance();
347
+ }
348
+ const value = _instance[prop as keyof Filegate];
349
+ if (typeof value === "function") {
350
+ return value.bind(_instance);
351
+ }
352
+ return value;
353
+ },
354
+ });
package/src/config.ts ADDED
@@ -0,0 +1,37 @@
1
+ const required = (key: string): string => {
2
+ const value = process.env[key];
3
+ if (!value) throw new Error(`Missing required env: ${key}`);
4
+ return value;
5
+ };
6
+
7
+ const int = (key: string, def: number): number => parseInt(process.env[key] ?? "", 10) || def;
8
+
9
+ const optionalInt = (key: string): number | undefined => {
10
+ const v = process.env[key];
11
+ return v ? parseInt(v, 10) : undefined;
12
+ };
13
+
14
+ const list = (key: string): string[] =>
15
+ required(key)
16
+ .split(",")
17
+ .map((s) => s.trim())
18
+ .filter(Boolean);
19
+
20
+ export const config = {
21
+ token: required("FILE_PROXY_TOKEN"),
22
+ allowedPaths: list("ALLOWED_BASE_PATHS"),
23
+ port: int("PORT", 4000),
24
+ maxUploadBytes: int("MAX_UPLOAD_MB", 500) * 1024 * 1024,
25
+ maxDownloadBytes: int("MAX_DOWNLOAD_MB", 5000) * 1024 * 1024,
26
+ maxChunkBytes: int("MAX_CHUNK_SIZE_MB", 50) * 1024 * 1024,
27
+ searchMaxResults: int("SEARCH_MAX_RESULTS", 100),
28
+ searchMaxRecursiveWildcards: int("SEARCH_MAX_RECURSIVE_WILDCARDS", 10),
29
+ uploadExpirySecs: int("UPLOAD_EXPIRY_HOURS", 24) * 60 * 60,
30
+ uploadTempDir: process.env.UPLOAD_TEMP_DIR ?? "/tmp/filegate-uploads",
31
+ diskCleanupIntervalMs: int("DISK_CLEANUP_INTERVAL_HOURS", 6) * 60 * 60 * 1000,
32
+ devUid: optionalInt("DEV_UID_OVERRIDE"),
33
+ devGid: optionalInt("DEV_GID_OVERRIDE"),
34
+ get isDev() {
35
+ return this.devUid !== undefined || this.devGid !== undefined;
36
+ },
37
+ } as const;