@happyvertical/files 0.74.8

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/index.js ADDED
@@ -0,0 +1,2719 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createWriteStream, existsSync, statSync, constants } from "node:fs";
3
+ import { rename, rm, lstat, realpath, writeFile, stat, chmod, chown, mkdir, readFile, readdir, access, rmdir, unlink, copyFile } from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import { join, dirname, basename, resolve, isAbsolute, relative, sep, extname } from "node:path";
6
+ import { Readable, Transform } from "node:stream";
7
+ import { pipeline } from "node:stream/promises";
8
+ import { getTempDirectory } from "@happyvertical/utils";
9
+ import { URL as URL$1 } from "node:url";
10
+ import { S3Client, ListObjectsV2Command, HeadObjectCommand, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, CopyObjectCommand } from "@aws-sdk/client-s3";
11
+ function createTempFilePath(filepath) {
12
+ return join(
13
+ dirname(filepath),
14
+ `.${basename(filepath)}.${randomUUID()}.download`
15
+ );
16
+ }
17
+ function isErrnoException(error) {
18
+ return error instanceof Error && "code" in error;
19
+ }
20
+ async function resolveDestinationPath(filepath) {
21
+ try {
22
+ const destinationStats = await lstat(filepath);
23
+ if (!destinationStats.isSymbolicLink()) {
24
+ return filepath;
25
+ }
26
+ return await realpath(filepath);
27
+ } catch (error) {
28
+ if (isErrnoException(error) && error.code === "ENOENT") {
29
+ return filepath;
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+ async function preserveExistingDestinationMetadata(sourcePath, tempFilepath) {
35
+ try {
36
+ const sourceStats = await stat(sourcePath);
37
+ await chmod(tempFilepath, sourceStats.mode);
38
+ try {
39
+ await chown(tempFilepath, sourceStats.uid, sourceStats.gid);
40
+ } catch (error) {
41
+ if (!isErrnoException(error) || !["EPERM", "EINVAL", "ENOSYS", "EROFS"].includes(error.code ?? "")) {
42
+ throw error;
43
+ }
44
+ }
45
+ } catch (error) {
46
+ if (!(isErrnoException(error) && error.code === "ENOENT")) {
47
+ throw error;
48
+ }
49
+ }
50
+ }
51
+ class MaxBytesTransform extends Transform {
52
+ constructor(maxBytes) {
53
+ super();
54
+ this.maxBytes = maxBytes;
55
+ }
56
+ totalBytes = 0;
57
+ _transform(chunk, _encoding, callback) {
58
+ this.totalBytes += chunk.byteLength;
59
+ if (this.totalBytes > this.maxBytes) {
60
+ callback(
61
+ new Error(
62
+ `Downloaded content exceeded maxBytes (${this.totalBytes} > ${this.maxBytes})`
63
+ )
64
+ );
65
+ return;
66
+ }
67
+ callback(null, chunk);
68
+ }
69
+ }
70
+ class RateLimiter {
71
+ /**
72
+ * Map of domains to their rate limit configurations
73
+ * Each domain tracks: lastRequest time, request limit, interval, and current queue size
74
+ */
75
+ domains = /* @__PURE__ */ new Map();
76
+ /**
77
+ * Default maximum number of requests per interval
78
+ * Applied to domains that don't have specific limits configured
79
+ */
80
+ defaultLimit = 6;
81
+ /**
82
+ * Default interval in milliseconds (500ms)
83
+ * Time window for the request limit enforcement
84
+ */
85
+ defaultInterval = 500;
86
+ getDefaultDomainConfig() {
87
+ const config = this.domains.get("default");
88
+ if (!config) {
89
+ throw new Error("Default domain rate limit configuration is missing");
90
+ }
91
+ return config;
92
+ }
93
+ /**
94
+ * Creates a new RateLimiter with default settings
95
+ * Initializes with a 'default' domain configuration used as fallback
96
+ */
97
+ constructor() {
98
+ this.domains.set("default", {
99
+ lastRequest: 0,
100
+ limit: this.defaultLimit,
101
+ interval: this.defaultInterval,
102
+ queue: 0
103
+ });
104
+ }
105
+ /**
106
+ * Extracts the domain from a URL for rate limiting purposes
107
+ *
108
+ * @param url - URL to extract domain from
109
+ * @returns Domain string (hostname) or 'default' if the URL is invalid
110
+ *
111
+ * @internal
112
+ */
113
+ getDomain(url) {
114
+ try {
115
+ return new URL(url).hostname;
116
+ } catch {
117
+ return "default";
118
+ }
119
+ }
120
+ /**
121
+ * Waits until the next request can be made according to rate limits
122
+ *
123
+ * This method implements the core rate limiting logic. It checks if the
124
+ * current request would exceed the domain's rate limit and delays if necessary.
125
+ *
126
+ * @param url - URL to check rate limits for (domain extracted automatically)
127
+ * @returns Promise that resolves when the request can proceed safely
128
+ *
129
+ * @internal
130
+ */
131
+ async waitForNext(url) {
132
+ const domain = this.getDomain(url);
133
+ const now = Date.now();
134
+ const domainConfig = this.domains.get(domain) || this.getDefaultDomainConfig();
135
+ if (domainConfig.queue >= domainConfig.limit) {
136
+ const timeToWait = Math.max(
137
+ 0,
138
+ domainConfig.lastRequest + domainConfig.interval - now
139
+ );
140
+ if (timeToWait > 0) {
141
+ await new Promise((resolve2) => setTimeout(resolve2, timeToWait));
142
+ }
143
+ domainConfig.queue = 0;
144
+ }
145
+ domainConfig.lastRequest = now;
146
+ domainConfig.queue++;
147
+ }
148
+ /**
149
+ * Sets rate limit for a specific domain
150
+ *
151
+ * @param domain - Domain to set limits for
152
+ * @param limit - Maximum number of requests per interval
153
+ * @param interval - Interval in milliseconds
154
+ */
155
+ setDomainLimit(domain, limit, interval) {
156
+ this.domains.set(domain, {
157
+ lastRequest: 0,
158
+ limit,
159
+ interval,
160
+ queue: 0
161
+ });
162
+ }
163
+ /**
164
+ * Gets rate limit configuration for a domain
165
+ *
166
+ * @param domain - Domain to get limits for
167
+ * @returns Rate limit configuration
168
+ */
169
+ getDomainLimit(domain) {
170
+ return this.domains.get(domain) || this.getDefaultDomainConfig();
171
+ }
172
+ }
173
+ const rateLimiter = new RateLimiter();
174
+ async function addRateLimit(domain, limit, interval) {
175
+ rateLimiter.setDomainLimit(domain, limit, interval);
176
+ }
177
+ async function getRateLimit(domain) {
178
+ const config = rateLimiter.getDomainLimit(domain);
179
+ return {
180
+ limit: config.limit,
181
+ interval: config.interval
182
+ };
183
+ }
184
+ async function rateLimitedFetch(url, options) {
185
+ await rateLimiter.waitForNext(url);
186
+ return fetch(url, options);
187
+ }
188
+ function buildFetchSignal(timeout, signal) {
189
+ if (timeout == null) {
190
+ return signal ?? void 0;
191
+ }
192
+ const timeoutSignal = AbortSignal.timeout(timeout);
193
+ if (!signal) {
194
+ return timeoutSignal;
195
+ }
196
+ return AbortSignal.any([signal, timeoutSignal]);
197
+ }
198
+ function assertOkResponse(response, url) {
199
+ if (!response.ok) {
200
+ throw new Error(
201
+ `Failed to fetch ${url}: ${response.status} ${response.statusText}`
202
+ );
203
+ }
204
+ }
205
+ async function fetchText(url) {
206
+ const response = await rateLimitedFetch(url);
207
+ assertOkResponse(response, url);
208
+ return response.text();
209
+ }
210
+ async function fetchJSON(url) {
211
+ const response = await rateLimitedFetch(url);
212
+ assertOkResponse(response, url);
213
+ return response.json();
214
+ }
215
+ async function fetchBuffer(url) {
216
+ const response = await rateLimitedFetch(url);
217
+ assertOkResponse(response, url);
218
+ return Buffer.from(await response.arrayBuffer());
219
+ }
220
+ async function fetchToFile(url, filepath, options = {}) {
221
+ const { timeout, maxBytes, signal, ...requestInit } = options;
222
+ const destinationPath = await resolveDestinationPath(filepath);
223
+ const response = await rateLimitedFetch(url, {
224
+ ...requestInit,
225
+ signal: buildFetchSignal(timeout, signal)
226
+ });
227
+ assertOkResponse(response, url);
228
+ await writeResponseToResolvedPath(response, destinationPath, { maxBytes });
229
+ }
230
+ async function writeResponseBodyToTempFile(response, tempFilepath, options = {}) {
231
+ const { maxBytes } = options;
232
+ if (!response.body) {
233
+ const buffer = Buffer.from(await response.arrayBuffer());
234
+ if (maxBytes != null && buffer.byteLength > maxBytes) {
235
+ throw new Error(
236
+ `Downloaded content exceeded maxBytes (${buffer.byteLength} > ${maxBytes})`
237
+ );
238
+ }
239
+ await writeFile(tempFilepath, buffer);
240
+ return;
241
+ }
242
+ const source = Readable.fromWeb(
243
+ response.body
244
+ );
245
+ const destination = createWriteStream(tempFilepath);
246
+ if (maxBytes != null) {
247
+ await pipeline(source, new MaxBytesTransform(maxBytes), destination);
248
+ } else {
249
+ await pipeline(source, destination);
250
+ }
251
+ }
252
+ async function writeResponseToResolvedPath(response, destinationPath, options = {}) {
253
+ const tempFilepath = createTempFilePath(destinationPath);
254
+ try {
255
+ await writeResponseBodyToTempFile(response, tempFilepath, options);
256
+ await preserveExistingDestinationMetadata(destinationPath, tempFilepath);
257
+ await rename(tempFilepath, destinationPath);
258
+ } catch (error) {
259
+ await rm(tempFilepath, { force: true }).catch(() => {
260
+ });
261
+ throw error;
262
+ }
263
+ }
264
+ async function writeResponseToFile(response, filepath, options = {}) {
265
+ const destinationPath = await resolveDestinationPath(filepath);
266
+ await writeResponseToResolvedPath(response, destinationPath, options);
267
+ }
268
+ class FilesystemAdapter {
269
+ /**
270
+ * Configuration options
271
+ */
272
+ options;
273
+ /**
274
+ * Cache directory path
275
+ */
276
+ cacheDir;
277
+ /**
278
+ * Creates a new FilesystemAdapter instance
279
+ *
280
+ * @param options - Configuration options
281
+ */
282
+ constructor(options) {
283
+ this.options = options;
284
+ this.cacheDir = options.cacheDir || getTempDirectory("cache");
285
+ }
286
+ /**
287
+ * Factory method to create and initialize a FilesystemAdapter
288
+ *
289
+ * @param options - Configuration options
290
+ * @returns Promise resolving to an initialized FilesystemAdapter
291
+ */
292
+ static async create(options) {
293
+ const fs = new FilesystemAdapter(options);
294
+ await fs.initialize();
295
+ return fs;
296
+ }
297
+ /**
298
+ * Initializes the adapter by creating the cache directory
299
+ */
300
+ async initialize() {
301
+ await mkdir(this.cacheDir, { recursive: true });
302
+ }
303
+ /**
304
+ * Downloads a file from a URL
305
+ *
306
+ * @param url - URL to download from
307
+ * @param options - Download options
308
+ * @param options.force - Whether to force download even if cached
309
+ * @returns Promise resolving to the path of the downloaded file
310
+ */
311
+ async download(_url, _options = {
312
+ force: false
313
+ }) {
314
+ return "";
315
+ }
316
+ /**
317
+ * Checks if a file or directory exists
318
+ *
319
+ * @param path - Path to check
320
+ * @returns Promise resolving to boolean indicating existence
321
+ */
322
+ async exists(_path) {
323
+ return false;
324
+ }
325
+ /**
326
+ * Reads a file's contents
327
+ *
328
+ * @param path - Path to the file
329
+ * @returns Promise resolving to the file contents as a string
330
+ */
331
+ async read(_path) {
332
+ return "";
333
+ }
334
+ /**
335
+ * Writes content to a file
336
+ *
337
+ * @param path - Path to the file
338
+ * @param content - Content to write
339
+ * @returns Promise that resolves when the write is complete
340
+ */
341
+ async write(_path, _content) {
342
+ }
343
+ /**
344
+ * Deletes a file or directory
345
+ *
346
+ * @param path - Path to delete
347
+ * @returns Promise that resolves when the deletion is complete
348
+ */
349
+ async delete(_path) {
350
+ }
351
+ /**
352
+ * Lists files in a directory
353
+ *
354
+ * @param path - Directory path to list
355
+ * @returns Promise resolving to an array of file names
356
+ */
357
+ async list(_path) {
358
+ return [];
359
+ }
360
+ /**
361
+ * Gets data from cache if available and not expired
362
+ *
363
+ * @param file - Cache file identifier
364
+ * @param expiry - Cache expiry time in milliseconds
365
+ * @returns Promise resolving to the cached data or undefined if not found/expired
366
+ */
367
+ async getCached(file, expiry = 3e5) {
368
+ return getCached(file, expiry);
369
+ }
370
+ /**
371
+ * Sets data in cache
372
+ *
373
+ * @param file - Cache file identifier
374
+ * @param data - Data to cache
375
+ * @returns Promise that resolves when the data is cached
376
+ */
377
+ async setCached(file, data) {
378
+ return setCached(file, data);
379
+ }
380
+ }
381
+ const TMP_DIR = path.resolve(getTempDirectory("kissd"));
382
+ const isFile = (file) => {
383
+ try {
384
+ const fileStat = statSync(file);
385
+ return fileStat.isDirectory() ? false : fileStat;
386
+ } catch {
387
+ return false;
388
+ }
389
+ };
390
+ const isDirectory = (dir) => {
391
+ try {
392
+ const dirStat = statSync(dir);
393
+ if (dirStat.isDirectory()) return true;
394
+ throw new Error(`${dir} exists but isn't a directory`);
395
+ } catch (error) {
396
+ if (error instanceof Error && error.message.includes("ENOENT")) {
397
+ return false;
398
+ }
399
+ throw error;
400
+ }
401
+ };
402
+ const ensureDirectoryExists = async (dir) => {
403
+ if (!isDirectory(dir)) {
404
+ console.log(`Creating directory: ${dir}`);
405
+ await mkdir(dir, { recursive: true });
406
+ }
407
+ };
408
+ const upload = async (url, data) => {
409
+ try {
410
+ const response = await fetch(url, {
411
+ method: "PUT",
412
+ body: Buffer.isBuffer(data) ? new Uint8Array(data) : data,
413
+ headers: { "Content-Type": "application/octet-stream" }
414
+ });
415
+ if (!response.ok) {
416
+ throw new Error(`unexpected response ${response.statusText}`);
417
+ }
418
+ return response;
419
+ } catch (error) {
420
+ const err = error;
421
+ console.error(`Error uploading data to ${url}
422
+ Error: ${err.message}`);
423
+ throw error;
424
+ }
425
+ };
426
+ async function download(url, filepath) {
427
+ try {
428
+ const response = await fetch(url);
429
+ if (!response.ok) {
430
+ throw new Error(`Unexpected response ${response.statusText}`);
431
+ }
432
+ const fileStream = createWriteStream(filepath);
433
+ return new Promise((resolve2, reject) => {
434
+ fileStream.on("error", reject);
435
+ fileStream.on("finish", resolve2);
436
+ response.body?.pipeTo(
437
+ new WritableStream({
438
+ write(chunk) {
439
+ fileStream.write(Buffer.from(chunk));
440
+ },
441
+ close() {
442
+ fileStream.end();
443
+ },
444
+ abort(reason) {
445
+ fileStream.destroy();
446
+ reject(reason);
447
+ }
448
+ })
449
+ ).catch(reject);
450
+ });
451
+ } catch (error) {
452
+ const err = error;
453
+ console.error("Error downloading file:", err);
454
+ throw error;
455
+ }
456
+ }
457
+ const downloadFileWithCache = async (url, targetPath = null) => {
458
+ const parsedUrl = new URL$1(url);
459
+ console.log(targetPath);
460
+ const downloadPath = targetPath || `${TMP_DIR}/downloads/${parsedUrl.hostname}${parsedUrl.pathname}`;
461
+ console.log("downloadPath", downloadPath);
462
+ if (!isFile(downloadPath)) {
463
+ await ensureDirectoryExists(dirname(downloadPath));
464
+ await download(url, downloadPath);
465
+ }
466
+ return downloadPath;
467
+ };
468
+ const listFiles = async (dirPath, options = { match: /.*/ }) => {
469
+ const entries = await readdir(dirPath, { withFileTypes: true });
470
+ const files = entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
471
+ return options.match ? files.filter((item) => options.match?.test(item)) : files;
472
+ };
473
+ async function getCached(file, expiry = 3e5) {
474
+ const cacheFile = path.resolve(TMP_DIR, file);
475
+ const cached = existsSync(cacheFile);
476
+ if (cached) {
477
+ const stats = statSync(cacheFile);
478
+ const modTime = new Date(stats.mtime);
479
+ const now = /* @__PURE__ */ new Date();
480
+ const isExpired = expiry && now.getTime() - modTime.getTime() > expiry;
481
+ if (!isExpired) {
482
+ return await readFile(cacheFile, "utf8");
483
+ }
484
+ }
485
+ }
486
+ async function setCached(file, data) {
487
+ const cacheFile = path.resolve(TMP_DIR, file);
488
+ await ensureDirectoryExists(path.dirname(cacheFile));
489
+ await writeFile(cacheFile, data);
490
+ }
491
+ const mimeTypes = {
492
+ ".html": "text/html",
493
+ ".js": "application/javascript",
494
+ ".json": "application/json",
495
+ ".css": "text/css",
496
+ ".png": "image/png",
497
+ ".jpg": "image/jpeg",
498
+ ".jpeg": "image/jpeg",
499
+ ".gif": "image/gif",
500
+ ".txt": "text/plain",
501
+ ".doc": "application/msword",
502
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
503
+ ".xls": "application/vnd.ms-excel",
504
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
505
+ ".pdf": "application/pdf",
506
+ ".xml": "application/xml",
507
+ ".zip": "application/zip",
508
+ ".rar": "application/x-rar-compressed",
509
+ ".mp3": "audio/mpeg",
510
+ ".mp4": "video/mp4",
511
+ ".avi": "video/x-msvideo",
512
+ ".mov": "video/quicktime"
513
+ // Add more mappings as needed
514
+ };
515
+ function getMimeType(fileOrUrl) {
516
+ const urlPattern = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//;
517
+ let extension;
518
+ if (urlPattern.test(fileOrUrl)) {
519
+ const url = new URL$1(fileOrUrl);
520
+ extension = path.extname(url.pathname);
521
+ } else {
522
+ extension = path.extname(fileOrUrl);
523
+ }
524
+ return mimeTypes[extension.toLowerCase()] || "application/octet-stream";
525
+ }
526
+ class FilesystemError extends Error {
527
+ constructor(message, code, path2, provider) {
528
+ super(message);
529
+ this.code = code;
530
+ this.path = path2;
531
+ this.provider = provider;
532
+ this.name = "FilesystemError";
533
+ }
534
+ }
535
+ class FileNotFoundError extends FilesystemError {
536
+ constructor(path2, provider) {
537
+ super(`File not found: ${path2}`, "ENOENT", path2, provider);
538
+ this.name = "FileNotFoundError";
539
+ }
540
+ }
541
+ class PermissionError extends FilesystemError {
542
+ constructor(path2, provider) {
543
+ super(`Permission denied: ${path2}`, "EACCES", path2, provider);
544
+ this.name = "PermissionError";
545
+ }
546
+ }
547
+ class DirectoryNotEmptyError extends FilesystemError {
548
+ constructor(path2, provider) {
549
+ super(`Directory not empty: ${path2}`, "ENOTEMPTY", path2, provider);
550
+ this.name = "DirectoryNotEmptyError";
551
+ }
552
+ }
553
+ class InvalidPathError extends FilesystemError {
554
+ constructor(path2, provider) {
555
+ super(`Invalid path: ${path2}`, "EINVAL", path2, provider);
556
+ this.name = "InvalidPathError";
557
+ }
558
+ }
559
+ class BaseFilesystemProvider {
560
+ basePath;
561
+ applyBasePath;
562
+ cacheDir;
563
+ createMissing;
564
+ providerType;
565
+ constructor(options = {}) {
566
+ this.basePath = options.basePath || "";
567
+ this.applyBasePath = options.applyBasePath ?? true;
568
+ this.cacheDir = options.cacheDir || this.getDefaultCacheDir();
569
+ this.createMissing = options.createMissing ?? true;
570
+ this.providerType = this.constructor.name.toLowerCase().replace("filesystemprovider", "");
571
+ }
572
+ /**
573
+ * Get default cache directory for the current context
574
+ */
575
+ getDefaultCacheDir() {
576
+ try {
577
+ const { getTempDirectory: getTempDirectory2 } = require("@happyvertical/utils");
578
+ return getTempDirectory2("files-cache");
579
+ } catch {
580
+ if (process?.versions?.node) {
581
+ try {
582
+ const { tmpdir } = require("node:os");
583
+ const { join: join2 } = require("node:path");
584
+ return join2(tmpdir(), "have-sdk", "files-cache");
585
+ } catch {
586
+ return "./tmp/have-sdk/files-cache";
587
+ }
588
+ }
589
+ return "./tmp/have-sdk/files-cache";
590
+ }
591
+ }
592
+ /**
593
+ * Throw error for unsupported operations
594
+ */
595
+ throwUnsupported(operation) {
596
+ throw new FilesystemError(
597
+ `Operation '${operation}' not supported by ${this.providerType} provider`,
598
+ "ENOTSUP",
599
+ void 0,
600
+ this.providerType
601
+ );
602
+ }
603
+ /**
604
+ * Normalize path by removing leading/trailing slashes and resolving relative paths
605
+ */
606
+ normalizePath(path2) {
607
+ if (!path2) return "";
608
+ let normalized = path2.startsWith("/") ? path2.slice(1) : path2;
609
+ if (this.applyBasePath && this.basePath) {
610
+ normalized = this.joinPaths(this.basePath, normalized);
611
+ }
612
+ return normalized;
613
+ }
614
+ /**
615
+ * Universal path joining function that works in both Node.js and browser
616
+ */
617
+ joinPaths(...paths) {
618
+ return paths.filter((p) => p && p.length > 0).map((p) => p.replace(/^\/+|\/+$/g, "")).join("/");
619
+ }
620
+ /**
621
+ * Validate that a path is safe (no directory traversal)
622
+ */
623
+ validatePath(path2) {
624
+ if (!path2) {
625
+ throw new FilesystemError("Path cannot be empty", "EINVAL", path2);
626
+ }
627
+ if (path2.includes("..") || path2.includes("~")) {
628
+ throw new FilesystemError(
629
+ "Path contains invalid characters (directory traversal)",
630
+ "EINVAL",
631
+ path2
632
+ );
633
+ }
634
+ }
635
+ /**
636
+ * Get cache key for a given path
637
+ */
638
+ getCacheKey(path2) {
639
+ return `${this.constructor.name}-${path2}`;
640
+ }
641
+ /**
642
+ * Provider methods with default implementations (may be overridden)
643
+ */
644
+ async upload(_localPath, _remotePath, _options = {}) {
645
+ this.throwUnsupported("upload");
646
+ }
647
+ async download(_remotePath, _localPath, _options = {}) {
648
+ this.throwUnsupported("download");
649
+ }
650
+ async downloadWithCache(remotePath, options = {}) {
651
+ const cacheKey = this.getCacheKey(remotePath);
652
+ if (!options.force) {
653
+ const cached = await this.cache.get(cacheKey, options.expiry);
654
+ if (cached) {
655
+ return cached;
656
+ }
657
+ }
658
+ const localPath = await this.download(remotePath, void 0, options);
659
+ await this.cache.set(cacheKey, localPath);
660
+ return localPath;
661
+ }
662
+ /**
663
+ * Cache implementation - providers can override for their specific storage
664
+ */
665
+ cache = {
666
+ get: async (_key, _expiry) => {
667
+ this.throwUnsupported("cache.get");
668
+ },
669
+ set: async (_key, _data) => {
670
+ this.throwUnsupported("cache.set");
671
+ },
672
+ clear: async (_key) => {
673
+ this.throwUnsupported("cache.clear");
674
+ }
675
+ };
676
+ // Legacy method implementations - providers can override or use default ENOTSUP errors
677
+ /**
678
+ * Check if a path is a file (legacy)
679
+ */
680
+ async isFile(file) {
681
+ try {
682
+ const stats = await this.getStats(file);
683
+ return stats.isFile ? stats : false;
684
+ } catch {
685
+ return false;
686
+ }
687
+ }
688
+ /**
689
+ * Check if a path is a directory (legacy)
690
+ */
691
+ async isDirectory(dir) {
692
+ try {
693
+ const stats = await this.getStats(dir);
694
+ return stats.isDirectory;
695
+ } catch {
696
+ return false;
697
+ }
698
+ }
699
+ /**
700
+ * Create a directory if it doesn't exist (legacy)
701
+ */
702
+ async ensureDirectoryExists(dir) {
703
+ if (!await this.isDirectory(dir)) {
704
+ await this.createDirectory(dir, { recursive: true });
705
+ }
706
+ }
707
+ /**
708
+ * Upload data to a URL using PUT method (legacy)
709
+ */
710
+ async uploadToUrl(_url, _data) {
711
+ this.throwUnsupported("uploadToUrl");
712
+ }
713
+ /**
714
+ * Download a file from a URL and save it to a local file (legacy)
715
+ */
716
+ async downloadFromUrl(_url, _filepath) {
717
+ this.throwUnsupported("downloadFromUrl");
718
+ }
719
+ /**
720
+ * Download a file with caching support (legacy)
721
+ */
722
+ async downloadFileWithCache(_url, _targetPath) {
723
+ this.throwUnsupported("downloadFileWithCache");
724
+ }
725
+ /**
726
+ * List files in a directory with optional filtering (legacy)
727
+ */
728
+ async listFiles(dirPath, options = { match: /.*/ }) {
729
+ const files = await this.list(dirPath);
730
+ const fileNames = files.filter((file) => !file.isDirectory).map((file) => file.name);
731
+ return options.match ? fileNames.filter((name) => options.match?.test(name)) : fileNames;
732
+ }
733
+ /**
734
+ * Get data from cache if available and not expired (legacy)
735
+ */
736
+ async getCached(file, expiry = 3e5) {
737
+ return await this.cache.get(file, expiry);
738
+ }
739
+ /**
740
+ * Set data in cache (legacy)
741
+ */
742
+ async setCached(file, data) {
743
+ await this.cache.set(file, data);
744
+ }
745
+ }
746
+ class LocalFilesystemProvider extends BaseFilesystemProvider {
747
+ rootPath;
748
+ constructor(options = {}) {
749
+ super({ ...options, applyBasePath: false });
750
+ this.rootPath = options.basePath ? resolve(options.basePath) : process.cwd();
751
+ }
752
+ /**
753
+ * Resolve a relative path to an absolute path within the root directory
754
+ *
755
+ * This method validates the path, normalizes it, and resolves it relative to
756
+ * the configured root path. It ensures path safety by preventing directory
757
+ * traversal attacks.
758
+ *
759
+ * @param path - Relative path to resolve
760
+ * @returns Absolute path within the root directory
761
+ * @throws {FilesystemError} When path is invalid or contains directory traversal
762
+ *
763
+ * @internal
764
+ */
765
+ resolvePath(path2) {
766
+ this.validatePath(path2);
767
+ const normalized = this.normalizePath(path2);
768
+ return join(this.rootPath, normalized);
769
+ }
770
+ resolveCachePath(path2) {
771
+ this.validatePath(path2);
772
+ if (isAbsolute(path2)) {
773
+ throw new InvalidPathError(path2, this.providerType);
774
+ }
775
+ const normalized = this.normalizePath(path2);
776
+ const cacheRoot = resolve(this.cacheDir);
777
+ const cachePath = resolve(cacheRoot, normalized);
778
+ const relativePath = relative(cacheRoot, cachePath);
779
+ if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${sep}`)) {
780
+ throw new InvalidPathError(path2, this.providerType);
781
+ }
782
+ return cachePath;
783
+ }
784
+ /**
785
+ * Check if a file or directory exists at the specified path
786
+ *
787
+ * Uses Node.js fs.access() to check for file existence without reading
788
+ * the file contents. This is more efficient than trying to read or stat
789
+ * the file when only existence needs to be verified.
790
+ *
791
+ * @param path - Path to check for existence
792
+ * @returns Promise resolving to true if the path exists, false otherwise
793
+ *
794
+ * @example
795
+ * ```typescript
796
+ * const exists = await fs.exists('config.json');
797
+ * if (exists) {
798
+ * console.log('Config file found');
799
+ * }
800
+ * ```
801
+ */
802
+ async exists(path2) {
803
+ try {
804
+ const resolvedPath = this.resolvePath(path2);
805
+ await access(resolvedPath, constants.F_OK);
806
+ return true;
807
+ } catch {
808
+ return false;
809
+ }
810
+ }
811
+ /**
812
+ * Read file contents as string or Buffer
813
+ *
814
+ * Reads the entire contents of a file into memory. For large files, consider
815
+ * using streaming approaches to avoid memory issues.
816
+ *
817
+ * @param path - Path to the file to read
818
+ * @param options - Read options including encoding and raw mode
819
+ * @param options.encoding - Text encoding for string output (default: 'utf8')
820
+ * @param options.raw - If true, returns Buffer instead of string
821
+ * @returns Promise resolving to file contents as string or Buffer
822
+ * @throws {FileNotFoundError} When the file doesn't exist
823
+ * @throws {PermissionError} When read access is denied
824
+ * @throws {FilesystemError} For other filesystem errors
825
+ *
826
+ * @example
827
+ * ```typescript
828
+ * // Read as UTF-8 string (default)
829
+ * const text = await fs.read('document.txt');
830
+ *
831
+ * // Read as Buffer for binary data
832
+ * const buffer = await fs.read('image.png', { raw: true });
833
+ *
834
+ * // Read with specific encoding
835
+ * const latin1Text = await fs.read('legacy.txt', { encoding: 'latin1' });
836
+ * ```
837
+ */
838
+ async read(path2, options = {}) {
839
+ try {
840
+ const resolvedPath = this.resolvePath(path2);
841
+ if (options.raw) {
842
+ return await readFile(resolvedPath);
843
+ }
844
+ return await readFile(resolvedPath, options.encoding || "utf8");
845
+ } catch (error) {
846
+ if (error.code === "ENOENT") {
847
+ throw new FileNotFoundError(path2, "local");
848
+ }
849
+ if (error.code === "EACCES") {
850
+ throw new PermissionError(path2, "local");
851
+ }
852
+ throw new FilesystemError(
853
+ `Failed to read file: ${error.message}`,
854
+ error.code || "UNKNOWN",
855
+ path2,
856
+ "local"
857
+ );
858
+ }
859
+ }
860
+ /**
861
+ * Write content to a file
862
+ *
863
+ * Creates or overwrites a file with the provided content. Can automatically
864
+ * create parent directories if they don't exist. Supports both string and
865
+ * binary data.
866
+ *
867
+ * @param path - Path where the file should be written
868
+ * @param content - Content to write (string or Buffer)
869
+ * @param options - Write options
870
+ * @param options.encoding - Text encoding for string content
871
+ * @param options.mode - File permissions (e.g., 0o644)
872
+ * @param options.createParents - Whether to create parent directories (default: true)
873
+ * @returns Promise that resolves when the file is written
874
+ * @throws {FileNotFoundError} When parent directory doesn't exist and createParents is false
875
+ * @throws {PermissionError} When write access is denied
876
+ * @throws {FilesystemError} For other filesystem errors
877
+ *
878
+ * @example
879
+ * ```typescript
880
+ * // Write text file
881
+ * await fs.write('config.json', JSON.stringify({ key: 'value' }));
882
+ *
883
+ * // Write binary data
884
+ * const imageBuffer = await someImageProcessing();
885
+ * await fs.write('output.png', imageBuffer);
886
+ *
887
+ * // Write with specific permissions
888
+ * await fs.write('secret.txt', 'sensitive data', { mode: 0o600 });
889
+ *
890
+ * // Write without creating parent directories
891
+ * await fs.write('existing/dir/file.txt', 'content', { createParents: false });
892
+ * ```
893
+ */
894
+ async write(path2, content, options = {}) {
895
+ try {
896
+ const resolvedPath = this.resolvePath(path2);
897
+ if (options.createParents ?? this.createMissing) {
898
+ await mkdir(dirname(resolvedPath), { recursive: true });
899
+ }
900
+ await writeFile(resolvedPath, content, {
901
+ encoding: options.encoding,
902
+ mode: options.mode
903
+ });
904
+ } catch (error) {
905
+ if (error.code === "ENOENT") {
906
+ throw new FileNotFoundError(dirname(path2), "local");
907
+ }
908
+ if (error.code === "EACCES") {
909
+ throw new PermissionError(path2, "local");
910
+ }
911
+ throw new FilesystemError(
912
+ `Failed to write file: ${error.message}`,
913
+ error.code || "UNKNOWN",
914
+ path2,
915
+ "local"
916
+ );
917
+ }
918
+ }
919
+ /**
920
+ * Delete a file or empty directory
921
+ *
922
+ * Removes a file or directory from the filesystem. For directories, they must
923
+ * be empty. Use recursive deletion utilities for non-empty directories.
924
+ *
925
+ * @param path - Path to the file or directory to delete
926
+ * @returns Promise that resolves when the item is deleted
927
+ * @throws {FileNotFoundError} When the file or directory doesn't exist
928
+ * @throws {PermissionError} When delete access is denied
929
+ * @throws {DirectoryNotEmptyError} When trying to delete a non-empty directory
930
+ * @throws {FilesystemError} For other filesystem errors
931
+ *
932
+ * @example
933
+ * ```typescript
934
+ * // Delete a file
935
+ * await fs.delete('temp.txt');
936
+ *
937
+ * // Delete an empty directory
938
+ * await fs.delete('empty-dir/');
939
+ * ```
940
+ */
941
+ async delete(path2) {
942
+ try {
943
+ const resolvedPath = this.resolvePath(path2);
944
+ const stats = await stat(resolvedPath);
945
+ if (stats.isDirectory()) {
946
+ await rmdir(resolvedPath);
947
+ } else {
948
+ await unlink(resolvedPath);
949
+ }
950
+ } catch (error) {
951
+ if (error.code === "ENOENT") {
952
+ throw new FileNotFoundError(path2, "local");
953
+ }
954
+ if (error.code === "EACCES") {
955
+ throw new PermissionError(path2, "local");
956
+ }
957
+ if (error.code === "ENOTEMPTY") {
958
+ throw new DirectoryNotEmptyError(path2, "local");
959
+ }
960
+ throw new FilesystemError(
961
+ `Failed to delete: ${error.message}`,
962
+ error.code || "UNKNOWN",
963
+ path2,
964
+ "local"
965
+ );
966
+ }
967
+ }
968
+ /**
969
+ * Copy a file from source to destination
970
+ *
971
+ * Creates an exact copy of a file at the destination path. Will create
972
+ * parent directories if they don't exist and createMissing is enabled.
973
+ * The operation preserves file contents but not necessarily all metadata.
974
+ *
975
+ * @param sourcePath - Path to the source file
976
+ * @param destPath - Path where the copy should be created
977
+ * @returns Promise that resolves when the file is copied
978
+ * @throws {FileNotFoundError} When the source file doesn't exist
979
+ * @throws {PermissionError} When read/write access is denied
980
+ * @throws {FilesystemError} For other filesystem errors
981
+ *
982
+ * @example
983
+ * ```typescript
984
+ * // Copy a file
985
+ * await fs.copy('original.txt', 'backup.txt');
986
+ *
987
+ * // Copy to a different directory
988
+ * await fs.copy('data.json', 'backup/data-copy.json');
989
+ * ```
990
+ */
991
+ async copy(sourcePath, destPath) {
992
+ try {
993
+ const resolvedSource = this.resolvePath(sourcePath);
994
+ const resolvedDest = this.resolvePath(destPath);
995
+ if (this.createMissing) {
996
+ await mkdir(dirname(resolvedDest), { recursive: true });
997
+ }
998
+ await copyFile(resolvedSource, resolvedDest);
999
+ } catch (error) {
1000
+ if (error.code === "ENOENT") {
1001
+ throw new FileNotFoundError(sourcePath, "local");
1002
+ }
1003
+ if (error.code === "EACCES") {
1004
+ throw new PermissionError(sourcePath, "local");
1005
+ }
1006
+ throw new FilesystemError(
1007
+ `Failed to copy: ${error.message}`,
1008
+ error.code || "UNKNOWN",
1009
+ sourcePath,
1010
+ "local"
1011
+ );
1012
+ }
1013
+ }
1014
+ /**
1015
+ * Move/rename a file from source to destination
1016
+ *
1017
+ * Moves a file to a new location, effectively combining copy and delete
1018
+ * operations. This is atomic on most filesystems when moving within the
1019
+ * same filesystem. Will create parent directories if they don't exist.
1020
+ *
1021
+ * @param sourcePath - Path to the source file
1022
+ * @param destPath - Path where the file should be moved
1023
+ * @returns Promise that resolves when the file is moved
1024
+ * @throws {FileNotFoundError} When the source file doesn't exist
1025
+ * @throws {PermissionError} When move access is denied
1026
+ * @throws {FilesystemError} For other filesystem errors
1027
+ *
1028
+ * @example
1029
+ * ```typescript
1030
+ * // Rename a file
1031
+ * await fs.move('old-name.txt', 'new-name.txt');
1032
+ *
1033
+ * // Move to a different directory
1034
+ * await fs.move('temp.txt', 'archive/temp.txt');
1035
+ * ```
1036
+ */
1037
+ async move(sourcePath, destPath) {
1038
+ try {
1039
+ const resolvedSource = this.resolvePath(sourcePath);
1040
+ const resolvedDest = this.resolvePath(destPath);
1041
+ if (this.createMissing) {
1042
+ await mkdir(dirname(resolvedDest), { recursive: true });
1043
+ }
1044
+ await rename(resolvedSource, resolvedDest);
1045
+ } catch (error) {
1046
+ if (error.code === "ENOENT") {
1047
+ throw new FileNotFoundError(sourcePath, "local");
1048
+ }
1049
+ if (error.code === "EACCES") {
1050
+ throw new PermissionError(sourcePath, "local");
1051
+ }
1052
+ throw new FilesystemError(
1053
+ `Failed to move: ${error.message}`,
1054
+ error.code || "UNKNOWN",
1055
+ sourcePath,
1056
+ "local"
1057
+ );
1058
+ }
1059
+ }
1060
+ /**
1061
+ * Create a directory at the specified path
1062
+ *
1063
+ * Creates a new directory with optional recursive creation of parent
1064
+ * directories. Supports setting directory permissions.
1065
+ *
1066
+ * @param path - Path where the directory should be created
1067
+ * @param options - Directory creation options
1068
+ * @param options.recursive - Whether to create parent directories (default: true)
1069
+ * @param options.mode - Directory permissions (e.g., 0o755)
1070
+ * @returns Promise that resolves when the directory is created
1071
+ * @throws {PermissionError} When create access is denied
1072
+ * @throws {FilesystemError} For other filesystem errors
1073
+ *
1074
+ * @example
1075
+ * ```typescript
1076
+ * // Create directory with parents
1077
+ * await fs.createDirectory('path/to/new/dir');
1078
+ *
1079
+ * // Create directory with specific permissions
1080
+ * await fs.createDirectory('private-dir', { mode: 0o700 });
1081
+ *
1082
+ * // Create directory without parent creation
1083
+ * await fs.createDirectory('existing-parent/new-dir', { recursive: false });
1084
+ * ```
1085
+ */
1086
+ async createDirectory(path2, options = {}) {
1087
+ try {
1088
+ const resolvedPath = this.resolvePath(path2);
1089
+ await mkdir(resolvedPath, {
1090
+ recursive: options.recursive ?? true,
1091
+ mode: options.mode
1092
+ });
1093
+ } catch (error) {
1094
+ if (error.code === "EACCES") {
1095
+ throw new PermissionError(path2, "local");
1096
+ }
1097
+ throw new FilesystemError(
1098
+ `Failed to create directory: ${error.message}`,
1099
+ error.code || "UNKNOWN",
1100
+ path2,
1101
+ "local"
1102
+ );
1103
+ }
1104
+ }
1105
+ /**
1106
+ * List the contents of a directory
1107
+ *
1108
+ * Returns an array of FileInfo objects describing each item in the directory.
1109
+ * Supports filtering, recursive listing, and detailed metadata retrieval.
1110
+ *
1111
+ * @param path - Path to the directory to list
1112
+ * @param options - Listing options
1113
+ * @param options.recursive - Whether to include subdirectories recursively
1114
+ * @param options.filter - RegExp or string pattern to filter file names
1115
+ * @param options.detailed - Whether to include MIME types and extended metadata
1116
+ * @returns Promise resolving to array of FileInfo objects
1117
+ * @throws {FileNotFoundError} When the directory doesn't exist
1118
+ * @throws {PermissionError} When read access is denied
1119
+ * @throws {FilesystemError} For other filesystem errors
1120
+ *
1121
+ * @example
1122
+ * ```typescript
1123
+ * // List all files and directories
1124
+ * const items = await fs.list('/path/to/dir');
1125
+ * items.forEach(item => {
1126
+ * console.log(`${item.name} (${item.isDirectory ? 'dir' : 'file'})`);
1127
+ * });
1128
+ *
1129
+ * // List only text files
1130
+ * const textFiles = await fs.list('/documents', { filter: /\.txt$/ });
1131
+ *
1132
+ * // Recursive listing with detailed info
1133
+ * const allFiles = await fs.list('/project', {
1134
+ * recursive: true,
1135
+ * detailed: true
1136
+ * });
1137
+ * ```
1138
+ */
1139
+ async list(path2, options = {}) {
1140
+ try {
1141
+ const resolvedPath = this.resolvePath(path2);
1142
+ const entries = await readdir(resolvedPath, { withFileTypes: true });
1143
+ const results = [];
1144
+ for (const entry of entries) {
1145
+ const fullPath = join(resolvedPath, entry.name);
1146
+ const relativePath = join(path2, entry.name);
1147
+ if (options.filter) {
1148
+ const filterPattern = typeof options.filter === "string" ? new RegExp(options.filter) : options.filter;
1149
+ if (!filterPattern.test(entry.name)) {
1150
+ continue;
1151
+ }
1152
+ }
1153
+ const stats = await stat(fullPath);
1154
+ const fileInfo = {
1155
+ name: entry.name,
1156
+ path: relativePath,
1157
+ size: stats.size,
1158
+ isDirectory: entry.isDirectory(),
1159
+ lastModified: stats.mtime,
1160
+ extension: entry.isFile() ? extname(entry.name).slice(1) : void 0
1161
+ };
1162
+ if (options.detailed) {
1163
+ fileInfo.mimeType = await this.getMimeType(relativePath);
1164
+ }
1165
+ results.push(fileInfo);
1166
+ if (options.recursive && entry.isDirectory()) {
1167
+ const subResults = await this.list(relativePath, options);
1168
+ results.push(...subResults);
1169
+ }
1170
+ }
1171
+ return results;
1172
+ } catch (error) {
1173
+ if (error.code === "ENOENT") {
1174
+ throw new FileNotFoundError(path2, "local");
1175
+ }
1176
+ if (error.code === "EACCES") {
1177
+ throw new PermissionError(path2, "local");
1178
+ }
1179
+ throw new FilesystemError(
1180
+ `Failed to list directory: ${error.message}`,
1181
+ error.code || "UNKNOWN",
1182
+ path2,
1183
+ "local"
1184
+ );
1185
+ }
1186
+ }
1187
+ /**
1188
+ * Get detailed file or directory statistics
1189
+ *
1190
+ * Returns comprehensive metadata about a file or directory including size,
1191
+ * timestamps, permissions, and ownership information.
1192
+ *
1193
+ * @param path - Path to the file or directory
1194
+ * @returns Promise resolving to FileStats object with detailed metadata
1195
+ * @throws {FileNotFoundError} When the path doesn't exist
1196
+ * @throws {PermissionError} When stat access is denied
1197
+ * @throws {FilesystemError} For other filesystem errors
1198
+ *
1199
+ * @example
1200
+ * ```typescript
1201
+ * const stats = await fs.getStats('document.pdf');
1202
+ * console.log(`Size: ${stats.size} bytes`);
1203
+ * console.log(`Modified: ${stats.mtime}`);
1204
+ * console.log(`Is file: ${stats.isFile}`);
1205
+ * console.log(`Permissions: ${stats.mode.toString(8)}`);
1206
+ * ```
1207
+ */
1208
+ async getStats(path2) {
1209
+ try {
1210
+ const resolvedPath = this.resolvePath(path2);
1211
+ const stats = await stat(resolvedPath);
1212
+ return {
1213
+ size: stats.size,
1214
+ isDirectory: stats.isDirectory(),
1215
+ isFile: stats.isFile(),
1216
+ birthtime: stats.birthtime,
1217
+ atime: stats.atime,
1218
+ mtime: stats.mtime,
1219
+ ctime: stats.ctime,
1220
+ mode: stats.mode,
1221
+ uid: stats.uid,
1222
+ gid: stats.gid
1223
+ };
1224
+ } catch (error) {
1225
+ if (error.code === "ENOENT") {
1226
+ throw new FileNotFoundError(path2, "local");
1227
+ }
1228
+ if (error.code === "EACCES") {
1229
+ throw new PermissionError(path2, "local");
1230
+ }
1231
+ throw new FilesystemError(
1232
+ `Failed to get stats: ${error.message}`,
1233
+ error.code || "UNKNOWN",
1234
+ path2,
1235
+ "local"
1236
+ );
1237
+ }
1238
+ }
1239
+ /**
1240
+ * Get the MIME type for a file based on its extension
1241
+ *
1242
+ * Determines the MIME type by examining the file extension. Returns a
1243
+ * standard MIME type string that can be used for content-type headers
1244
+ * or file type detection.
1245
+ *
1246
+ * @param path - Path to the file
1247
+ * @returns Promise resolving to MIME type string (e.g., 'text/plain', 'image/png')
1248
+ *
1249
+ * @example
1250
+ * ```typescript
1251
+ * const mimeType = await fs.getMimeType('document.pdf');
1252
+ * console.log(mimeType); // 'application/pdf'
1253
+ *
1254
+ * const imageMime = await fs.getMimeType('photo.jpg');
1255
+ * console.log(imageMime); // 'image/jpeg'
1256
+ * ```
1257
+ */
1258
+ async getMimeType(path2) {
1259
+ const mimeTypes2 = {
1260
+ ".html": "text/html",
1261
+ ".js": "application/javascript",
1262
+ ".json": "application/json",
1263
+ ".css": "text/css",
1264
+ ".png": "image/png",
1265
+ ".jpg": "image/jpeg",
1266
+ ".jpeg": "image/jpeg",
1267
+ ".gif": "image/gif",
1268
+ ".txt": "text/plain",
1269
+ ".doc": "application/msword",
1270
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1271
+ ".xls": "application/vnd.ms-excel",
1272
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1273
+ ".pdf": "application/pdf",
1274
+ ".xml": "application/xml",
1275
+ ".zip": "application/zip",
1276
+ ".rar": "application/x-rar-compressed",
1277
+ ".mp3": "audio/mpeg",
1278
+ ".mp4": "video/mp4",
1279
+ ".avi": "video/x-msvideo",
1280
+ ".mov": "video/quicktime"
1281
+ };
1282
+ const extension = extname(path2).toLowerCase();
1283
+ return mimeTypes2[extension] || "application/octet-stream";
1284
+ }
1285
+ /**
1286
+ * Upload data to a URL using PUT method (legacy)
1287
+ */
1288
+ async uploadToUrl(url, data) {
1289
+ try {
1290
+ const response = await fetch(url, {
1291
+ method: "PUT",
1292
+ body: Buffer.isBuffer(data) ? new Uint8Array(data) : data,
1293
+ headers: { "Content-Type": "application/octet-stream" }
1294
+ });
1295
+ if (!response.ok) {
1296
+ throw new Error(`unexpected response ${response.statusText}`);
1297
+ }
1298
+ return response;
1299
+ } catch (error) {
1300
+ const err = error;
1301
+ console.error(`Error uploading data to ${url}
1302
+ Error: ${err.message}`);
1303
+ throw error;
1304
+ }
1305
+ }
1306
+ /**
1307
+ * Download a file from a URL and save it to a local file (legacy)
1308
+ */
1309
+ async downloadFromUrl(url, filepath) {
1310
+ try {
1311
+ const response = await fetch(url);
1312
+ if (!response.ok) {
1313
+ throw new Error(`Unexpected response ${response.statusText}`);
1314
+ }
1315
+ const fileStream = createWriteStream(this.resolvePath(filepath));
1316
+ return new Promise((resolve2, reject) => {
1317
+ fileStream.on("error", reject);
1318
+ fileStream.on("finish", resolve2);
1319
+ response.body?.pipeTo(
1320
+ new WritableStream({
1321
+ write(chunk) {
1322
+ fileStream.write(Buffer.from(chunk));
1323
+ },
1324
+ close() {
1325
+ fileStream.end();
1326
+ },
1327
+ abort(reason) {
1328
+ fileStream.destroy();
1329
+ reject(reason);
1330
+ }
1331
+ })
1332
+ ).catch(reject);
1333
+ });
1334
+ } catch (error) {
1335
+ const err = error;
1336
+ console.error("Error downloading file:", err);
1337
+ throw error;
1338
+ }
1339
+ }
1340
+ /**
1341
+ * Download a file with caching support (legacy)
1342
+ */
1343
+ async downloadFileWithCache(url, targetPath = null) {
1344
+ const parsedUrl = new URL$1(url);
1345
+ const downloadPath = targetPath || join(
1346
+ getTempDirectory("downloads"),
1347
+ parsedUrl.hostname + parsedUrl.pathname
1348
+ );
1349
+ if (!existsSync(downloadPath)) {
1350
+ await mkdir(dirname(downloadPath), { recursive: true });
1351
+ await this.downloadFromUrl(url, downloadPath);
1352
+ }
1353
+ return downloadPath;
1354
+ }
1355
+ /**
1356
+ * Get data from cache if available and not expired (legacy)
1357
+ */
1358
+ async getCached(file, expiry = 3e5) {
1359
+ const cacheFile = this.resolveCachePath(file);
1360
+ const cached = existsSync(cacheFile);
1361
+ if (cached) {
1362
+ const stats = statSync(cacheFile);
1363
+ const modTime = new Date(stats.mtime);
1364
+ const now = /* @__PURE__ */ new Date();
1365
+ const isExpired = expiry && now.getTime() - modTime.getTime() > expiry;
1366
+ if (!isExpired) {
1367
+ return await readFile(cacheFile, "utf8");
1368
+ }
1369
+ }
1370
+ return void 0;
1371
+ }
1372
+ /**
1373
+ * Set data in cache (legacy)
1374
+ */
1375
+ async setCached(file, data) {
1376
+ const cacheFile = this.resolveCachePath(file);
1377
+ await mkdir(dirname(cacheFile), { recursive: true });
1378
+ await writeFile(cacheFile, data);
1379
+ }
1380
+ /**
1381
+ * Cache implementation using file system
1382
+ */
1383
+ cache = {
1384
+ get: async (key, expiry) => {
1385
+ return await this.getCached(key, expiry);
1386
+ },
1387
+ set: async (key, data) => {
1388
+ await this.setCached(key, data);
1389
+ },
1390
+ clear: async (key) => {
1391
+ if (key) {
1392
+ const cacheFile = this.resolveCachePath(key);
1393
+ try {
1394
+ await unlink(cacheFile);
1395
+ } catch {
1396
+ }
1397
+ } else {
1398
+ try {
1399
+ await rmdir(this.cacheDir, { recursive: true });
1400
+ } catch {
1401
+ }
1402
+ }
1403
+ }
1404
+ };
1405
+ /**
1406
+ * Get provider capabilities
1407
+ */
1408
+ async getCapabilities() {
1409
+ return {
1410
+ streaming: true,
1411
+ atomicOperations: true,
1412
+ versioning: false,
1413
+ sharing: false,
1414
+ realTimeSync: false,
1415
+ offlineCapable: true,
1416
+ supportedOperations: [
1417
+ "exists",
1418
+ "read",
1419
+ "write",
1420
+ "delete",
1421
+ "copy",
1422
+ "move",
1423
+ "createDirectory",
1424
+ "list",
1425
+ "getStats",
1426
+ "getMimeType",
1427
+ "upload",
1428
+ "download",
1429
+ "downloadWithCache",
1430
+ "isFile",
1431
+ "isDirectory",
1432
+ "ensureDirectoryExists",
1433
+ "uploadToUrl",
1434
+ "downloadFromUrl",
1435
+ "downloadFileWithCache",
1436
+ "listFiles",
1437
+ "getCached",
1438
+ "setCached"
1439
+ ]
1440
+ };
1441
+ }
1442
+ }
1443
+ const local = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1444
+ __proto__: null,
1445
+ LocalFilesystemProvider
1446
+ }, Symbol.toStringTag, { value: "Module" }));
1447
+ const MIME_TYPES$1 = {
1448
+ ".html": "text/html",
1449
+ ".js": "application/javascript",
1450
+ ".json": "application/json",
1451
+ ".css": "text/css",
1452
+ ".png": "image/png",
1453
+ ".jpg": "image/jpeg",
1454
+ ".jpeg": "image/jpeg",
1455
+ ".gif": "image/gif",
1456
+ ".svg": "image/svg+xml",
1457
+ ".txt": "text/plain",
1458
+ ".md": "text/markdown",
1459
+ ".csv": "text/csv",
1460
+ ".doc": "application/msword",
1461
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1462
+ ".xls": "application/vnd.ms-excel",
1463
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1464
+ ".ppt": "application/vnd.ms-powerpoint",
1465
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1466
+ ".pdf": "application/pdf",
1467
+ ".xml": "application/xml",
1468
+ ".zip": "application/zip",
1469
+ ".mp3": "audio/mpeg",
1470
+ ".mp4": "video/mp4"
1471
+ };
1472
+ const GOOGLE_EXPORT_MIMES = {
1473
+ "application/vnd.google-apps.document": "text/plain",
1474
+ "application/vnd.google-apps.spreadsheet": "text/csv",
1475
+ "application/vnd.google-apps.presentation": "application/pdf",
1476
+ "application/vnd.google-apps.drawing": "image/png"
1477
+ };
1478
+ class GoogleDriveProvider extends BaseFilesystemProvider {
1479
+ constructor(options) {
1480
+ super(options);
1481
+ this.options = options;
1482
+ this.rootFolderId = options.folderId || "root";
1483
+ this.pathCacheTTL = options.pathCacheTTL ?? 3e5;
1484
+ this.pageSize = options.pageSize ?? 100;
1485
+ }
1486
+ drive;
1487
+ rootFolderId;
1488
+ pathCache = /* @__PURE__ */ new Map();
1489
+ pathCacheTTL;
1490
+ pageSize;
1491
+ /**
1492
+ * Lazily initialize the Google Drive client on first use
1493
+ */
1494
+ async getDrive() {
1495
+ if (this.drive) return this.drive;
1496
+ const { google } = await import("googleapis");
1497
+ let auth;
1498
+ if (this.options.serviceAccountKey) {
1499
+ const { GoogleAuth } = await import("google-auth-library");
1500
+ const key = JSON.parse(this.options.serviceAccountKey);
1501
+ auth = new GoogleAuth({
1502
+ credentials: key,
1503
+ scopes: this.options.scopes || [
1504
+ "https://www.googleapis.com/auth/drive"
1505
+ ]
1506
+ });
1507
+ } else if (this.options.clientId && this.options.clientSecret && this.options.refreshToken) {
1508
+ const oauth2 = new google.auth.OAuth2(
1509
+ this.options.clientId,
1510
+ this.options.clientSecret
1511
+ );
1512
+ oauth2.setCredentials({
1513
+ refresh_token: this.options.refreshToken,
1514
+ access_token: this.options.accessToken
1515
+ });
1516
+ auth = oauth2;
1517
+ } else if (this.options.accessToken) {
1518
+ const oauth2 = new google.auth.OAuth2();
1519
+ oauth2.setCredentials({ access_token: this.options.accessToken });
1520
+ auth = oauth2;
1521
+ } else {
1522
+ throw new FilesystemError(
1523
+ "Google Drive provider requires OAuth2 credentials, a serviceAccountKey, or an accessToken",
1524
+ "EINVAL",
1525
+ void 0,
1526
+ "gdrive"
1527
+ );
1528
+ }
1529
+ this.drive = google.drive({ version: "v3", auth });
1530
+ return this.drive;
1531
+ }
1532
+ // ---------------------------------------------------------------------------
1533
+ // Path ↔ File-ID resolution
1534
+ // ---------------------------------------------------------------------------
1535
+ /**
1536
+ * Resolve a hierarchical path like "folder/sub/file.txt" to a Drive file ID
1537
+ * by walking each segment. Results are cached with a configurable TTL.
1538
+ */
1539
+ async resolvePathToId(path2) {
1540
+ const normalized = this.normalizePath(path2);
1541
+ if (!normalized || normalized === ".") return this.rootFolderId;
1542
+ const cached = this.pathCache.get(normalized);
1543
+ if (cached && Date.now() - cached.ts < this.pathCacheTTL) {
1544
+ return cached.id;
1545
+ }
1546
+ const segments = normalized.split("/").filter(Boolean);
1547
+ let parentId = this.rootFolderId;
1548
+ for (const segment of segments) {
1549
+ const drive = await this.getDrive();
1550
+ const q = `'${parentId}' in parents and name = '${segment.replace(/'/g, "\\'")}' and trashed = false`;
1551
+ const res = await this.wrapApiCall(
1552
+ () => drive.files.list({
1553
+ q,
1554
+ fields: "files(id, name)",
1555
+ pageSize: 1
1556
+ }),
1557
+ path2
1558
+ );
1559
+ if (!res.data.files || res.data.files.length === 0) {
1560
+ return null;
1561
+ }
1562
+ parentId = res.data.files[0].id;
1563
+ }
1564
+ this.pathCache.set(normalized, { id: parentId, ts: Date.now() });
1565
+ return parentId;
1566
+ }
1567
+ /**
1568
+ * Resolve a path to an ID, throwing FileNotFoundError if it doesn't exist
1569
+ */
1570
+ async requireId(path2) {
1571
+ const id = await this.resolvePathToId(path2);
1572
+ if (!id) throw new FileNotFoundError(path2, "gdrive");
1573
+ return id;
1574
+ }
1575
+ /**
1576
+ * Ensure all parent folders exist for a given path, creating them if needed.
1577
+ * Returns the parent folder ID and the final segment name.
1578
+ */
1579
+ async ensureParents(path2) {
1580
+ const normalized = this.normalizePath(path2);
1581
+ const segments = normalized.split("/").filter(Boolean);
1582
+ const name = segments.pop();
1583
+ let parentId = this.rootFolderId;
1584
+ let currentPath = "";
1585
+ for (const segment of segments) {
1586
+ currentPath = currentPath ? `${currentPath}/${segment}` : segment;
1587
+ const existingId = await this.resolvePathToId(currentPath);
1588
+ if (existingId) {
1589
+ parentId = existingId;
1590
+ continue;
1591
+ }
1592
+ const drive = await this.getDrive();
1593
+ const res = await this.wrapApiCall(
1594
+ () => drive.files.create({
1595
+ requestBody: {
1596
+ name: segment,
1597
+ mimeType: "application/vnd.google-apps.folder",
1598
+ parents: [parentId]
1599
+ },
1600
+ fields: "id"
1601
+ }),
1602
+ path2
1603
+ );
1604
+ parentId = res.data.id;
1605
+ this.pathCache.set(currentPath, { id: parentId, ts: Date.now() });
1606
+ }
1607
+ return { parentId, name };
1608
+ }
1609
+ /**
1610
+ * Invalidate cache entries that are prefixed by the given path
1611
+ */
1612
+ invalidateCache(path2) {
1613
+ const normalized = this.normalizePath(path2);
1614
+ for (const key of this.pathCache.keys()) {
1615
+ if (key === normalized || key.startsWith(normalized + "/")) {
1616
+ this.pathCache.delete(key);
1617
+ }
1618
+ }
1619
+ }
1620
+ // ---------------------------------------------------------------------------
1621
+ // API error mapping
1622
+ // ---------------------------------------------------------------------------
1623
+ /**
1624
+ * Wrap a Drive API call and map HTTP errors to typed filesystem errors
1625
+ */
1626
+ async wrapApiCall(fn, path2) {
1627
+ try {
1628
+ return await fn();
1629
+ } catch (error) {
1630
+ const status = error?.code ?? error?.response?.status;
1631
+ if (status === 404) {
1632
+ throw new FileNotFoundError(path2, "gdrive");
1633
+ }
1634
+ if (status === 403) {
1635
+ throw new PermissionError(path2, "gdrive");
1636
+ }
1637
+ if (status === 401) {
1638
+ throw new FilesystemError(
1639
+ `Authentication failed: ${error.message}`,
1640
+ "EACCES",
1641
+ path2,
1642
+ "gdrive"
1643
+ );
1644
+ }
1645
+ if (status === 429) {
1646
+ throw new FilesystemError(
1647
+ `Rate limited: ${error.message}`,
1648
+ "EAGAIN",
1649
+ path2,
1650
+ "gdrive"
1651
+ );
1652
+ }
1653
+ throw new FilesystemError(
1654
+ `Google Drive API error: ${error.message}`,
1655
+ error.code?.toString() || "UNKNOWN",
1656
+ path2,
1657
+ "gdrive"
1658
+ );
1659
+ }
1660
+ }
1661
+ // ---------------------------------------------------------------------------
1662
+ // FilesystemInterface implementation
1663
+ // ---------------------------------------------------------------------------
1664
+ /** Checks whether a non-trashed file or folder exists at the given path. */
1665
+ async exists(path2) {
1666
+ const id = await this.resolvePathToId(path2);
1667
+ return id !== null;
1668
+ }
1669
+ /**
1670
+ * Read a file from Google Drive. Google Docs native types (Document,
1671
+ * Spreadsheet, Presentation, Drawing) are automatically exported to a
1672
+ * portable format (plain text, CSV, PDF, PNG respectively).
1673
+ */
1674
+ async read(path2, options = {}) {
1675
+ const fileId = await this.requireId(path2);
1676
+ const drive = await this.getDrive();
1677
+ const meta = await this.wrapApiCall(
1678
+ () => drive.files.get({ fileId, fields: "mimeType" }),
1679
+ path2
1680
+ );
1681
+ const mimeType = meta.data.mimeType;
1682
+ const exportMime = GOOGLE_EXPORT_MIMES[mimeType];
1683
+ let data;
1684
+ if (exportMime) {
1685
+ const res = await this.wrapApiCall(
1686
+ () => drive.files.export(
1687
+ { fileId, mimeType: exportMime },
1688
+ { responseType: "arraybuffer" }
1689
+ ),
1690
+ path2
1691
+ );
1692
+ data = Buffer.from(res.data);
1693
+ } else {
1694
+ const res = await this.wrapApiCall(
1695
+ () => drive.files.get(
1696
+ { fileId, alt: "media" },
1697
+ { responseType: "arraybuffer" }
1698
+ ),
1699
+ path2
1700
+ );
1701
+ data = Buffer.from(res.data);
1702
+ }
1703
+ if (options.raw) {
1704
+ return data;
1705
+ }
1706
+ return data.toString(options.encoding || "utf8");
1707
+ }
1708
+ /** Write content to Drive. Updates the file in-place if it already exists, otherwise creates it. */
1709
+ async write(path2, content, options = {}) {
1710
+ const body = Buffer.isBuffer(content) ? content : Buffer.from(content);
1711
+ const ext = extname(path2).toLowerCase();
1712
+ const contentMime = MIME_TYPES$1[ext] || "application/octet-stream";
1713
+ const existingId = await this.resolvePathToId(path2);
1714
+ const drive = await this.getDrive();
1715
+ if (existingId && existingId !== this.rootFolderId) {
1716
+ await this.wrapApiCall(
1717
+ () => drive.files.update({
1718
+ fileId: existingId,
1719
+ media: { mimeType: contentMime, body }
1720
+ }),
1721
+ path2
1722
+ );
1723
+ } else {
1724
+ if (options.createParents ?? this.createMissing) {
1725
+ const { parentId, name } = await this.ensureParents(path2);
1726
+ await this.wrapApiCall(
1727
+ () => drive.files.create({
1728
+ requestBody: {
1729
+ name,
1730
+ parents: [parentId]
1731
+ },
1732
+ media: { mimeType: contentMime, body },
1733
+ fields: "id"
1734
+ }),
1735
+ path2
1736
+ );
1737
+ } else {
1738
+ const parentPath = dirname(this.normalizePath(path2));
1739
+ const parentId = parentPath === "." || parentPath === "" ? this.rootFolderId : await this.requireId(parentPath);
1740
+ const name = basename(path2);
1741
+ await this.wrapApiCall(
1742
+ () => drive.files.create({
1743
+ requestBody: {
1744
+ name,
1745
+ parents: [parentId]
1746
+ },
1747
+ media: { mimeType: contentMime, body },
1748
+ fields: "id"
1749
+ }),
1750
+ path2
1751
+ );
1752
+ }
1753
+ }
1754
+ this.invalidateCache(path2);
1755
+ }
1756
+ /** Moves the file to the Drive trash rather than permanently deleting it. */
1757
+ async delete(path2) {
1758
+ const fileId = await this.requireId(path2);
1759
+ const drive = await this.getDrive();
1760
+ await this.wrapApiCall(
1761
+ () => drive.files.update({
1762
+ fileId,
1763
+ requestBody: { trashed: true }
1764
+ }),
1765
+ path2
1766
+ );
1767
+ this.invalidateCache(path2);
1768
+ }
1769
+ async copy(sourcePath, destPath) {
1770
+ const sourceId = await this.requireId(sourcePath);
1771
+ const drive = await this.getDrive();
1772
+ const { parentId, name } = await this.ensureParents(destPath);
1773
+ await this.wrapApiCall(
1774
+ () => drive.files.copy({
1775
+ fileId: sourceId,
1776
+ requestBody: {
1777
+ name,
1778
+ parents: [parentId]
1779
+ },
1780
+ fields: "id"
1781
+ }),
1782
+ sourcePath
1783
+ );
1784
+ this.invalidateCache(destPath);
1785
+ }
1786
+ /** Moves a file by updating its parent references (single API call, not copy+delete). */
1787
+ async move(sourcePath, destPath) {
1788
+ const sourceId = await this.requireId(sourcePath);
1789
+ const drive = await this.getDrive();
1790
+ const fileMeta = await this.wrapApiCall(
1791
+ () => drive.files.get({ fileId: sourceId, fields: "parents" }),
1792
+ sourcePath
1793
+ );
1794
+ const previousParents = (fileMeta.data.parents || []).join(",");
1795
+ const { parentId, name } = await this.ensureParents(destPath);
1796
+ await this.wrapApiCall(
1797
+ () => drive.files.update({
1798
+ fileId: sourceId,
1799
+ addParents: parentId,
1800
+ removeParents: previousParents,
1801
+ requestBody: { name },
1802
+ fields: "id, parents"
1803
+ }),
1804
+ sourcePath
1805
+ );
1806
+ this.invalidateCache(sourcePath);
1807
+ this.invalidateCache(destPath);
1808
+ }
1809
+ async createDirectory(path2, options = {}) {
1810
+ const normalized = this.normalizePath(path2);
1811
+ const segments = normalized.split("/").filter(Boolean);
1812
+ if (options.recursive ?? true) {
1813
+ let currentPath = "";
1814
+ for (const segment of segments) {
1815
+ currentPath = currentPath ? `${currentPath}/${segment}` : segment;
1816
+ const existingId = await this.resolvePathToId(currentPath);
1817
+ if (!existingId) {
1818
+ const parentPath = dirname(currentPath);
1819
+ const parentId = parentPath === "." || parentPath === "" ? this.rootFolderId : await this.requireId(parentPath);
1820
+ const drive = await this.getDrive();
1821
+ const res = await this.wrapApiCall(
1822
+ () => drive.files.create({
1823
+ requestBody: {
1824
+ name: segment,
1825
+ mimeType: "application/vnd.google-apps.folder",
1826
+ parents: [parentId]
1827
+ },
1828
+ fields: "id"
1829
+ }),
1830
+ path2
1831
+ );
1832
+ this.pathCache.set(currentPath, {
1833
+ id: res.data.id,
1834
+ ts: Date.now()
1835
+ });
1836
+ }
1837
+ }
1838
+ } else {
1839
+ const parentPath = dirname(normalized);
1840
+ const parentId = parentPath === "." || parentPath === "" ? this.rootFolderId : await this.requireId(parentPath);
1841
+ const name = segments[segments.length - 1];
1842
+ const drive = await this.getDrive();
1843
+ const res = await this.wrapApiCall(
1844
+ () => drive.files.create({
1845
+ requestBody: {
1846
+ name,
1847
+ mimeType: "application/vnd.google-apps.folder",
1848
+ parents: [parentId]
1849
+ },
1850
+ fields: "id"
1851
+ }),
1852
+ path2
1853
+ );
1854
+ this.pathCache.set(normalized, { id: res.data.id, ts: Date.now() });
1855
+ }
1856
+ }
1857
+ /** Lists non-trashed children of a folder, with optional pagination via {@link GoogleDriveOptions.pageSize}. */
1858
+ async list(path2, options = {}) {
1859
+ const folderId = !path2 || path2 === "." || path2 === "/" ? this.rootFolderId : await this.requireId(path2);
1860
+ const drive = await this.getDrive();
1861
+ const results = [];
1862
+ let pageToken;
1863
+ do {
1864
+ const res = await this.wrapApiCall(
1865
+ () => drive.files.list({
1866
+ q: `'${folderId}' in parents and trashed = false`,
1867
+ fields: "nextPageToken, files(id, name, mimeType, size, modifiedTime)",
1868
+ pageSize: this.pageSize,
1869
+ pageToken
1870
+ }),
1871
+ path2
1872
+ );
1873
+ const files = res.data.files || [];
1874
+ for (const file of files) {
1875
+ const isDir = file.mimeType === "application/vnd.google-apps.folder";
1876
+ const name = file.name;
1877
+ const filePath = path2 && path2 !== "." && path2 !== "/" ? `${this.normalizePath(path2)}/${name}` : name;
1878
+ if (options.filter) {
1879
+ const filterPattern = typeof options.filter === "string" ? new RegExp(options.filter) : options.filter;
1880
+ if (!filterPattern.test(name)) continue;
1881
+ }
1882
+ const ext = isDir ? void 0 : extname(name).slice(1) || void 0;
1883
+ const fileInfo = {
1884
+ name,
1885
+ path: filePath,
1886
+ size: Number(file.size || 0),
1887
+ isDirectory: isDir,
1888
+ lastModified: new Date(file.modifiedTime),
1889
+ extension: ext
1890
+ };
1891
+ if (options.detailed) {
1892
+ fileInfo.mimeType = file.mimeType;
1893
+ }
1894
+ results.push(fileInfo);
1895
+ this.pathCache.set(filePath, { id: file.id, ts: Date.now() });
1896
+ if (options.recursive && isDir) {
1897
+ const subResults = await this.list(filePath, options);
1898
+ results.push(...subResults);
1899
+ }
1900
+ }
1901
+ pageToken = res.data.nextPageToken ?? void 0;
1902
+ } while (pageToken);
1903
+ return results;
1904
+ }
1905
+ /** Returns file metadata. Mode is synthetic (0o755 for folders, 0o644 for files); uid/gid are always 0. */
1906
+ async getStats(path2) {
1907
+ const fileId = await this.requireId(path2);
1908
+ const drive = await this.getDrive();
1909
+ const res = await this.wrapApiCall(
1910
+ () => drive.files.get({
1911
+ fileId,
1912
+ fields: "id, name, mimeType, size, createdTime, modifiedTime"
1913
+ }),
1914
+ path2
1915
+ );
1916
+ const file = res.data;
1917
+ const isDir = file.mimeType === "application/vnd.google-apps.folder";
1918
+ const modifiedTime = new Date(file.modifiedTime);
1919
+ const createdTime = new Date(file.createdTime);
1920
+ return {
1921
+ size: Number(file.size || 0),
1922
+ isDirectory: isDir,
1923
+ isFile: !isDir,
1924
+ birthtime: createdTime,
1925
+ atime: modifiedTime,
1926
+ mtime: modifiedTime,
1927
+ ctime: modifiedTime,
1928
+ mode: isDir ? 493 : 420,
1929
+ uid: 0,
1930
+ gid: 0
1931
+ };
1932
+ }
1933
+ async getMimeType(path2) {
1934
+ const ext = extname(path2).toLowerCase();
1935
+ return MIME_TYPES$1[ext] || "application/octet-stream";
1936
+ }
1937
+ async upload(localPath, remotePath, _options = {}) {
1938
+ const content = await readFile(localPath);
1939
+ await this.write(remotePath, content);
1940
+ }
1941
+ /** Downloads a file from Drive to the local filesystem, defaulting to the provider's cache directory. */
1942
+ async download(remotePath, localPath, _options = {}) {
1943
+ const content = await this.read(remotePath, { raw: true });
1944
+ const target = localPath || join(this.cacheDir, this.normalizePath(remotePath));
1945
+ await mkdir(dirname(target), { recursive: true });
1946
+ await writeFile(target, content);
1947
+ return target;
1948
+ }
1949
+ async getCapabilities() {
1950
+ return {
1951
+ streaming: false,
1952
+ atomicOperations: false,
1953
+ versioning: true,
1954
+ sharing: true,
1955
+ realTimeSync: true,
1956
+ offlineCapable: false,
1957
+ maxFileSize: 5 * 1024 * 1024 * 1024 * 1024,
1958
+ // 5 TB
1959
+ supportedOperations: [
1960
+ "exists",
1961
+ "read",
1962
+ "write",
1963
+ "delete",
1964
+ "copy",
1965
+ "move",
1966
+ "createDirectory",
1967
+ "list",
1968
+ "getStats",
1969
+ "getMimeType",
1970
+ "upload",
1971
+ "download"
1972
+ ]
1973
+ };
1974
+ }
1975
+ }
1976
+ const gdrive = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1977
+ __proto__: null,
1978
+ GoogleDriveProvider
1979
+ }, Symbol.toStringTag, { value: "Module" }));
1980
+ const MIME_TYPES = {
1981
+ ".txt": "text/plain",
1982
+ ".json": "application/json",
1983
+ ".csv": "text/csv",
1984
+ ".svg": "image/svg+xml",
1985
+ ".png": "image/png",
1986
+ ".jpg": "image/jpeg",
1987
+ ".jpeg": "image/jpeg",
1988
+ ".webp": "image/webp",
1989
+ ".pdf": "application/pdf",
1990
+ ".mp4": "video/mp4",
1991
+ ".webm": "video/webm"
1992
+ };
1993
+ function toDate(value) {
1994
+ return value instanceof Date ? value : /* @__PURE__ */ new Date();
1995
+ }
1996
+ function toCopySource(bucket, key) {
1997
+ return `${bucket}/${key.split("/").map((segment) => encodeURIComponent(segment)).join("/")}`;
1998
+ }
1999
+ class S3FilesystemProvider extends BaseFilesystemProvider {
2000
+ constructor(options) {
2001
+ super(options);
2002
+ this.options = options;
2003
+ this.bucket = options.bucket;
2004
+ this.client = new S3Client({
2005
+ region: options.region,
2006
+ endpoint: options.endpoint,
2007
+ forcePathStyle: options.forcePathStyle,
2008
+ credentials: options.accessKeyId && options.secretAccessKey ? {
2009
+ accessKeyId: options.accessKeyId,
2010
+ secretAccessKey: options.secretAccessKey
2011
+ } : void 0
2012
+ });
2013
+ }
2014
+ client;
2015
+ bucket;
2016
+ normalizeS3Path(path2, options = {}) {
2017
+ const normalized = path2 === "." || path2 === "/" ? this.basePath.replace(/^\/+|\/+$/g, "") : this.normalizePath(path2).replace(/^\/+|\/+$/g, "");
2018
+ if (!normalized) {
2019
+ return "";
2020
+ }
2021
+ if (options.preserveTrailingSlash && path2 !== "." && path2 !== "/" && /\/+$/.test(path2)) {
2022
+ return `${normalized}/`;
2023
+ }
2024
+ return normalized;
2025
+ }
2026
+ toDirectoryKey(path2) {
2027
+ const directoryPath = path2.endsWith("/") ? path2 : `${path2}/`;
2028
+ return this.normalizeS3Path(directoryPath, {
2029
+ preserveTrailingSlash: true
2030
+ });
2031
+ }
2032
+ isRootPath(path2) {
2033
+ return path2 === "." || path2 === "/";
2034
+ }
2035
+ toDirectoryStats(lastModified) {
2036
+ const timestamp = toDate(lastModified);
2037
+ return {
2038
+ size: 0,
2039
+ isDirectory: true,
2040
+ isFile: false,
2041
+ birthtime: timestamp,
2042
+ atime: timestamp,
2043
+ mtime: timestamp,
2044
+ ctime: timestamp,
2045
+ mode: 0,
2046
+ uid: 0,
2047
+ gid: 0
2048
+ };
2049
+ }
2050
+ async headDirectoryMarker(path2) {
2051
+ const directoryKey = this.toDirectoryKey(path2);
2052
+ if (!directoryKey) {
2053
+ return null;
2054
+ }
2055
+ try {
2056
+ return await this.head(directoryKey);
2057
+ } catch (error) {
2058
+ if (error instanceof FileNotFoundError) {
2059
+ return null;
2060
+ }
2061
+ throw error;
2062
+ }
2063
+ }
2064
+ async directoryHasChildren(directoryKey) {
2065
+ const listing = await this.client.send(
2066
+ new ListObjectsV2Command({
2067
+ Bucket: this.bucket,
2068
+ Prefix: directoryKey,
2069
+ MaxKeys: 2
2070
+ })
2071
+ );
2072
+ return (listing.Contents || []).some(
2073
+ (entry) => entry.Key && entry.Key !== directoryKey
2074
+ );
2075
+ }
2076
+ getDefaultDownloadTarget(remotePath) {
2077
+ const normalizedRemotePath = this.normalizeS3Path(remotePath, {
2078
+ preserveTrailingSlash: remotePath.endsWith("/")
2079
+ });
2080
+ const segments = normalizedRemotePath.split("/").filter(Boolean);
2081
+ if (!segments.length) {
2082
+ throw new InvalidPathError(remotePath, "s3");
2083
+ }
2084
+ if (segments.some(
2085
+ (segment) => segment === ".." || segment === "~" || segment.startsWith("~") || /^[A-Za-z]:/.test(segment)
2086
+ )) {
2087
+ throw new InvalidPathError(remotePath, "s3");
2088
+ }
2089
+ const baseDir = resolve(this.cacheDir);
2090
+ const target = resolve(baseDir, ...segments);
2091
+ const targetRelativePath = relative(baseDir, target);
2092
+ if (!targetRelativePath || targetRelativePath === ".." || targetRelativePath.startsWith(`..${sep}`)) {
2093
+ throw new InvalidPathError(remotePath, "s3");
2094
+ }
2095
+ return target;
2096
+ }
2097
+ async bodyToBuffer(body) {
2098
+ if (!body) return Buffer.alloc(0);
2099
+ if (Buffer.isBuffer(body)) return body;
2100
+ if (body instanceof Uint8Array) return Buffer.from(body);
2101
+ if (typeof body.transformToByteArray === "function") {
2102
+ const bytes = await body.transformToByteArray();
2103
+ return Buffer.from(bytes);
2104
+ }
2105
+ if (typeof body.getReader === "function") {
2106
+ const reader = body.getReader();
2107
+ const chunks = [];
2108
+ while (true) {
2109
+ const { done, value } = await reader.read();
2110
+ if (done) break;
2111
+ if (value) chunks.push(Buffer.from(value));
2112
+ }
2113
+ return Buffer.concat(chunks);
2114
+ }
2115
+ if (Symbol.asyncIterator in Object(body)) {
2116
+ const chunks = [];
2117
+ for await (const chunk of body) {
2118
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2119
+ }
2120
+ return Buffer.concat(chunks);
2121
+ }
2122
+ return Buffer.from(String(body));
2123
+ }
2124
+ async head(key) {
2125
+ try {
2126
+ return await this.client.send(
2127
+ new HeadObjectCommand({
2128
+ Bucket: this.bucket,
2129
+ Key: key
2130
+ })
2131
+ );
2132
+ } catch (error) {
2133
+ if (error?.$metadata?.httpStatusCode === 404 || error?.name === "NotFound") {
2134
+ throw new FileNotFoundError(key, "s3");
2135
+ }
2136
+ if (error?.$metadata?.httpStatusCode === 403) {
2137
+ throw new PermissionError(key, "s3");
2138
+ }
2139
+ throw new FilesystemError(
2140
+ `Failed to stat S3 object: ${error instanceof Error ? error.message : String(error)}`,
2141
+ error?.name || "UNKNOWN",
2142
+ key,
2143
+ "s3"
2144
+ );
2145
+ }
2146
+ }
2147
+ toFileInfo(key, entry, isDirectory2 = false) {
2148
+ const normalized = key.replace(/\/$/, "");
2149
+ const name = normalized.split("/").pop() || normalized;
2150
+ const extension = isDirectory2 ? void 0 : extname(normalized).replace(/^\./, "");
2151
+ const mimeType = isDirectory2 ? void 0 : MIME_TYPES[extname(normalized).toLowerCase()] || "application/octet-stream";
2152
+ return {
2153
+ name,
2154
+ path: normalized,
2155
+ size: Number(entry.Size || 0),
2156
+ isDirectory: isDirectory2,
2157
+ lastModified: toDate(entry.LastModified),
2158
+ mimeType,
2159
+ extension
2160
+ };
2161
+ }
2162
+ async exists(path2) {
2163
+ if (this.isRootPath(path2)) {
2164
+ return true;
2165
+ }
2166
+ const key = this.normalizeS3Path(path2, {
2167
+ preserveTrailingSlash: path2.endsWith("/")
2168
+ });
2169
+ if (!key) {
2170
+ return true;
2171
+ }
2172
+ try {
2173
+ await this.head(key);
2174
+ return true;
2175
+ } catch (error) {
2176
+ if (error instanceof FileNotFoundError) {
2177
+ const directoryMarker = await this.headDirectoryMarker(path2);
2178
+ if (directoryMarker) {
2179
+ return true;
2180
+ }
2181
+ const listing = await this.client.send(
2182
+ new ListObjectsV2Command({
2183
+ Bucket: this.bucket,
2184
+ Prefix: `${key}/`,
2185
+ MaxKeys: 1
2186
+ })
2187
+ );
2188
+ return Boolean((listing.Contents || []).length);
2189
+ }
2190
+ throw error;
2191
+ }
2192
+ }
2193
+ async read(path2, options = {}) {
2194
+ const key = this.normalizeS3Path(path2, {
2195
+ preserveTrailingSlash: path2.endsWith("/")
2196
+ });
2197
+ try {
2198
+ const response = await this.client.send(
2199
+ new GetObjectCommand({
2200
+ Bucket: this.bucket,
2201
+ Key: key
2202
+ })
2203
+ );
2204
+ const buffer = await this.bodyToBuffer(response.Body);
2205
+ if (options.raw) {
2206
+ return buffer;
2207
+ }
2208
+ return buffer.toString(options.encoding || "utf8");
2209
+ } catch (error) {
2210
+ if (error?.$metadata?.httpStatusCode === 404 || error?.name === "NoSuchKey") {
2211
+ throw new FileNotFoundError(path2, "s3");
2212
+ }
2213
+ if (error?.$metadata?.httpStatusCode === 403) {
2214
+ throw new PermissionError(path2, "s3");
2215
+ }
2216
+ throw new FilesystemError(
2217
+ `Failed to read S3 object: ${error instanceof Error ? error.message : String(error)}`,
2218
+ error?.name || "UNKNOWN",
2219
+ path2,
2220
+ "s3"
2221
+ );
2222
+ }
2223
+ }
2224
+ async write(path2, content, options = {}) {
2225
+ const key = this.normalizeS3Path(path2, {
2226
+ preserveTrailingSlash: path2.endsWith("/")
2227
+ });
2228
+ const body = typeof content === "string" ? Buffer.from(content, options.encoding || "utf8") : content;
2229
+ try {
2230
+ await this.client.send(
2231
+ new PutObjectCommand({
2232
+ Bucket: this.bucket,
2233
+ Key: key,
2234
+ Body: body,
2235
+ ContentType: MIME_TYPES[extname(key).toLowerCase()] || "application/octet-stream"
2236
+ })
2237
+ );
2238
+ } catch (error) {
2239
+ if (error?.$metadata?.httpStatusCode === 403) {
2240
+ throw new PermissionError(path2, "s3");
2241
+ }
2242
+ throw new FilesystemError(
2243
+ `Failed to write S3 object: ${error instanceof Error ? error.message : String(error)}`,
2244
+ error?.name || "UNKNOWN",
2245
+ path2,
2246
+ "s3"
2247
+ );
2248
+ }
2249
+ }
2250
+ async delete(path2) {
2251
+ let key = this.normalizeS3Path(path2, {
2252
+ preserveTrailingSlash: path2.endsWith("/")
2253
+ });
2254
+ try {
2255
+ if (key) {
2256
+ try {
2257
+ const stats = await this.getStats(path2);
2258
+ if (stats.isDirectory) {
2259
+ key = this.toDirectoryKey(path2);
2260
+ if (key && await this.directoryHasChildren(key)) {
2261
+ throw new DirectoryNotEmptyError(path2, "s3");
2262
+ }
2263
+ }
2264
+ } catch (error) {
2265
+ if (!(error instanceof FileNotFoundError)) {
2266
+ throw error;
2267
+ }
2268
+ }
2269
+ }
2270
+ await this.client.send(
2271
+ new DeleteObjectCommand({
2272
+ Bucket: this.bucket,
2273
+ Key: key
2274
+ })
2275
+ );
2276
+ } catch (error) {
2277
+ if (error instanceof FilesystemError) {
2278
+ throw error;
2279
+ }
2280
+ throw new FilesystemError(
2281
+ `Failed to delete S3 object: ${error instanceof Error ? error.message : String(error)}`,
2282
+ error?.name || "UNKNOWN",
2283
+ path2,
2284
+ "s3"
2285
+ );
2286
+ }
2287
+ }
2288
+ async copy(sourcePath, destPath) {
2289
+ const sourceKey = this.normalizeS3Path(sourcePath, {
2290
+ preserveTrailingSlash: sourcePath.endsWith("/")
2291
+ });
2292
+ const destKey = this.normalizeS3Path(destPath, {
2293
+ preserveTrailingSlash: destPath.endsWith("/")
2294
+ });
2295
+ try {
2296
+ await this.client.send(
2297
+ new CopyObjectCommand({
2298
+ Bucket: this.bucket,
2299
+ Key: destKey,
2300
+ CopySource: toCopySource(this.bucket, sourceKey)
2301
+ })
2302
+ );
2303
+ } catch (error) {
2304
+ throw new FilesystemError(
2305
+ `Failed to copy S3 object: ${error instanceof Error ? error.message : String(error)}`,
2306
+ error?.name || "UNKNOWN",
2307
+ destPath,
2308
+ "s3"
2309
+ );
2310
+ }
2311
+ }
2312
+ async move(sourcePath, destPath) {
2313
+ await this.copy(sourcePath, destPath);
2314
+ await this.delete(sourcePath);
2315
+ }
2316
+ async createDirectory(path2, _options = {}) {
2317
+ if (!this.toDirectoryKey(path2)) {
2318
+ return;
2319
+ }
2320
+ const directoryPath = path2.endsWith("/") ? path2 : `${path2}/`;
2321
+ await this.write(directoryPath, Buffer.alloc(0));
2322
+ }
2323
+ async list(path2, options = {}) {
2324
+ const prefix = this.normalizeS3Path(path2, {
2325
+ preserveTrailingSlash: path2.endsWith("/")
2326
+ });
2327
+ const prefixWithSlash = prefix ? `${prefix.replace(/\/+$/, "")}/` : "";
2328
+ const items = [];
2329
+ const seenDirectories = /* @__PURE__ */ new Set();
2330
+ const addDirectory = (key, lastModified) => {
2331
+ const directoryKey = key.endsWith("/") ? key : `${key}/`;
2332
+ const normalized = directoryKey.replace(/\/$/, "");
2333
+ if (!normalized || seenDirectories.has(normalized)) {
2334
+ return;
2335
+ }
2336
+ seenDirectories.add(normalized);
2337
+ items.push(
2338
+ this.toFileInfo(directoryKey, { LastModified: lastModified }, true)
2339
+ );
2340
+ };
2341
+ let continuationToken;
2342
+ do {
2343
+ const response = await this.client.send(
2344
+ new ListObjectsV2Command({
2345
+ Bucket: this.bucket,
2346
+ Prefix: prefixWithSlash,
2347
+ Delimiter: options.recursive ? void 0 : "/",
2348
+ ContinuationToken: continuationToken
2349
+ })
2350
+ );
2351
+ for (const prefixEntry of response.CommonPrefixes || []) {
2352
+ if (prefixEntry.Prefix) {
2353
+ addDirectory(prefixEntry.Prefix);
2354
+ }
2355
+ }
2356
+ for (const entry of response.Contents || []) {
2357
+ if (!entry.Key || entry.Key === prefixWithSlash) continue;
2358
+ const relative2 = prefixWithSlash ? entry.Key.slice(prefixWithSlash.length) : entry.Key;
2359
+ if (entry.Key.endsWith("/")) {
2360
+ addDirectory(entry.Key, entry.LastModified);
2361
+ continue;
2362
+ }
2363
+ if (!options.recursive && relative2.includes("/")) {
2364
+ const directory = relative2.split("/")[0];
2365
+ addDirectory(`${prefixWithSlash}${directory}`, entry.LastModified);
2366
+ continue;
2367
+ }
2368
+ items.push(this.toFileInfo(entry.Key, entry));
2369
+ }
2370
+ continuationToken = response.IsTruncated ? response.NextContinuationToken : void 0;
2371
+ } while (continuationToken);
2372
+ return items.filter((item) => {
2373
+ if (!options.filter) return true;
2374
+ const matcher = typeof options.filter === "string" ? new RegExp(options.filter) : options.filter;
2375
+ return matcher.test(item.name);
2376
+ });
2377
+ }
2378
+ async getStats(path2) {
2379
+ if (this.isRootPath(path2)) {
2380
+ return this.toDirectoryStats();
2381
+ }
2382
+ const key = this.normalizeS3Path(path2, {
2383
+ preserveTrailingSlash: path2.endsWith("/")
2384
+ });
2385
+ if (!key) {
2386
+ return this.toDirectoryStats();
2387
+ }
2388
+ try {
2389
+ const response = await this.head(key);
2390
+ return {
2391
+ size: Number(response.ContentLength || 0),
2392
+ isDirectory: key.endsWith("/"),
2393
+ isFile: !key.endsWith("/"),
2394
+ birthtime: toDate(response.LastModified),
2395
+ atime: toDate(response.LastModified),
2396
+ mtime: toDate(response.LastModified),
2397
+ ctime: toDate(response.LastModified),
2398
+ mode: 0,
2399
+ uid: 0,
2400
+ gid: 0
2401
+ };
2402
+ } catch (error) {
2403
+ if (error instanceof FileNotFoundError) {
2404
+ const directoryMarker = await this.headDirectoryMarker(path2);
2405
+ if (directoryMarker) {
2406
+ return this.toDirectoryStats(directoryMarker.LastModified);
2407
+ }
2408
+ const listing = await this.client.send(
2409
+ new ListObjectsV2Command({
2410
+ Bucket: this.bucket,
2411
+ Prefix: `${key.replace(/\/+$/, "")}/`,
2412
+ MaxKeys: 1
2413
+ })
2414
+ );
2415
+ if ((listing.Contents || []).length) {
2416
+ const [firstEntry] = listing.Contents || [];
2417
+ return this.toDirectoryStats(firstEntry?.LastModified);
2418
+ }
2419
+ }
2420
+ throw error;
2421
+ }
2422
+ }
2423
+ async getMimeType(path2) {
2424
+ const key = this.normalizeS3Path(path2, {
2425
+ preserveTrailingSlash: path2.endsWith("/")
2426
+ });
2427
+ try {
2428
+ const response = await this.head(key);
2429
+ return response.ContentType || MIME_TYPES[extname(key).toLowerCase()] || "application/octet-stream";
2430
+ } catch (error) {
2431
+ if (error instanceof FileNotFoundError) {
2432
+ return MIME_TYPES[extname(key).toLowerCase()] || "application/octet-stream";
2433
+ }
2434
+ throw error;
2435
+ }
2436
+ }
2437
+ async upload(localPath, remotePath, _options = {}) {
2438
+ const { readFile: readFile2 } = await import("node:fs/promises");
2439
+ await this.write(remotePath, await readFile2(localPath));
2440
+ }
2441
+ async download(remotePath, localPath, _options = {}) {
2442
+ const buffer = await this.read(remotePath, { raw: true });
2443
+ const target = localPath || this.getDefaultDownloadTarget(remotePath);
2444
+ await mkdir(dirname(target), { recursive: true });
2445
+ await writeFile(target, buffer);
2446
+ return target;
2447
+ }
2448
+ async getCapabilities() {
2449
+ return {
2450
+ streaming: false,
2451
+ atomicOperations: false,
2452
+ versioning: false,
2453
+ sharing: false,
2454
+ realTimeSync: false,
2455
+ offlineCapable: false,
2456
+ supportedOperations: [
2457
+ "exists",
2458
+ "read",
2459
+ "write",
2460
+ "delete",
2461
+ "copy",
2462
+ "move",
2463
+ "createDirectory",
2464
+ "list",
2465
+ "getStats",
2466
+ "getMimeType",
2467
+ "upload",
2468
+ "download"
2469
+ ]
2470
+ };
2471
+ }
2472
+ }
2473
+ const s3 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
2474
+ __proto__: null,
2475
+ S3FilesystemProvider
2476
+ }, Symbol.toStringTag, { value: "Module" }));
2477
+ const SECRET_KEY_FRAGMENTS = [
2478
+ "apikey",
2479
+ "api_key",
2480
+ "clientsecret",
2481
+ "client_secret",
2482
+ "credential",
2483
+ "privatekey",
2484
+ "private_key",
2485
+ "secret",
2486
+ "password",
2487
+ "token"
2488
+ ];
2489
+ const EXACT_SECRET_KEYS = /* @__PURE__ */ new Set(["serviceaccountkey", "accesskeyid"]);
2490
+ function looksLikeSecret(key) {
2491
+ const normalized = key.toLowerCase();
2492
+ if (EXACT_SECRET_KEYS.has(normalized)) return true;
2493
+ return SECRET_KEY_FRAGMENTS.some((fragment) => normalized.includes(fragment));
2494
+ }
2495
+ function redactFilesystemConfig(value) {
2496
+ return redactSecrets(value, /* @__PURE__ */ new WeakSet(), /* @__PURE__ */ new WeakMap());
2497
+ }
2498
+ function redactSecrets(value, inProgress, done) {
2499
+ if (!value || typeof value !== "object") return value;
2500
+ const obj = value;
2501
+ if (done.has(obj)) return done.get(obj);
2502
+ if (inProgress.has(obj)) return "[circular]";
2503
+ inProgress.add(obj);
2504
+ let result;
2505
+ if (Array.isArray(value)) {
2506
+ result = value.map((item) => redactSecrets(item, inProgress, done));
2507
+ } else {
2508
+ result = Object.fromEntries(
2509
+ Object.entries(value).map(([key, item]) => [
2510
+ key,
2511
+ looksLikeSecret(key) ? "[redacted]" : redactSecrets(item, inProgress, done)
2512
+ ])
2513
+ );
2514
+ }
2515
+ inProgress.delete(obj);
2516
+ done.set(obj, result);
2517
+ return result;
2518
+ }
2519
+ const providers = /* @__PURE__ */ new Map();
2520
+ function registerProvider(type, factory2) {
2521
+ providers.set(type, factory2);
2522
+ }
2523
+ function getAvailableProviders() {
2524
+ return Array.from(providers.keys());
2525
+ }
2526
+ function validateOptions(options) {
2527
+ if (!options) {
2528
+ throw new FilesystemError("Provider options are required", "EINVAL");
2529
+ }
2530
+ const type = options.type || "local";
2531
+ switch (type) {
2532
+ case "local":
2533
+ break;
2534
+ case "s3": {
2535
+ const s3Opts = options;
2536
+ if (!s3Opts.region) {
2537
+ throw new FilesystemError("S3 provider requires region", "EINVAL");
2538
+ }
2539
+ if (!s3Opts.bucket) {
2540
+ throw new FilesystemError("S3 provider requires bucket", "EINVAL");
2541
+ }
2542
+ break;
2543
+ }
2544
+ case "gdrive": {
2545
+ const gdriveOpts = options;
2546
+ const hasOAuth2 = gdriveOpts.clientId && gdriveOpts.clientSecret && gdriveOpts.refreshToken;
2547
+ const hasServiceAccount = !!gdriveOpts.serviceAccountKey;
2548
+ const hasAccessToken = !!gdriveOpts.accessToken;
2549
+ if (!hasOAuth2 && !hasServiceAccount && !hasAccessToken) {
2550
+ throw new FilesystemError(
2551
+ "Google Drive provider requires OAuth2 credentials (clientId + clientSecret + refreshToken), a serviceAccountKey, or an accessToken",
2552
+ "EINVAL"
2553
+ );
2554
+ }
2555
+ break;
2556
+ }
2557
+ case "webdav": {
2558
+ const webdavOpts = options;
2559
+ if (!webdavOpts.baseUrl) {
2560
+ throw new FilesystemError("WebDAV provider requires baseUrl", "EINVAL");
2561
+ }
2562
+ if (!webdavOpts.username) {
2563
+ throw new FilesystemError(
2564
+ "WebDAV provider requires username",
2565
+ "EINVAL"
2566
+ );
2567
+ }
2568
+ if (!webdavOpts.password) {
2569
+ throw new FilesystemError(
2570
+ "WebDAV provider requires password",
2571
+ "EINVAL"
2572
+ );
2573
+ }
2574
+ break;
2575
+ }
2576
+ case "browser-storage":
2577
+ break;
2578
+ default:
2579
+ throw new FilesystemError(`Unknown provider type: ${type}`, "EINVAL");
2580
+ }
2581
+ }
2582
+ function detectProviderType(options) {
2583
+ if (options.type) {
2584
+ return options.type;
2585
+ }
2586
+ if ("region" in options && "bucket" in options) {
2587
+ return "s3";
2588
+ }
2589
+ if ("clientId" in options && "clientSecret" in options || "serviceAccountKey" in options || "accessToken" in options) {
2590
+ return "gdrive";
2591
+ }
2592
+ if ("baseUrl" in options && "username" in options) {
2593
+ return "webdav";
2594
+ }
2595
+ if ("databaseName" in options || "storageQuota" in options) {
2596
+ return "browser-storage";
2597
+ }
2598
+ if (typeof globalThis !== "undefined") {
2599
+ if (typeof globalThis.window !== "undefined" && typeof globalThis.indexedDB !== "undefined") {
2600
+ return "browser-storage";
2601
+ }
2602
+ if (globalThis.process?.versions?.node) {
2603
+ return "local";
2604
+ }
2605
+ }
2606
+ return "local";
2607
+ }
2608
+ async function getFilesystem(options = {}) {
2609
+ validateOptions(options);
2610
+ const type = detectProviderType(options);
2611
+ const providerFactory = providers.get(type);
2612
+ if (!providerFactory) {
2613
+ throw new FilesystemError(
2614
+ `Provider '${type}' is not registered. Available providers: ${getAvailableProviders().join(", ")}`,
2615
+ "ENOTFOUND"
2616
+ );
2617
+ }
2618
+ try {
2619
+ const ProviderClass = await providerFactory();
2620
+ return new ProviderClass(options);
2621
+ } catch (error) {
2622
+ throw new FilesystemError(
2623
+ `Failed to create '${type}' provider: ${error instanceof Error ? error.message : String(error)}`,
2624
+ "ENOENT",
2625
+ void 0,
2626
+ type
2627
+ );
2628
+ }
2629
+ }
2630
+ async function initializeProviders() {
2631
+ registerProvider("local", async () => {
2632
+ const { LocalFilesystemProvider: LocalFilesystemProvider2 } = await Promise.resolve().then(() => local);
2633
+ return LocalFilesystemProvider2;
2634
+ });
2635
+ registerProvider("s3", async () => {
2636
+ const { S3FilesystemProvider: S3FilesystemProvider2 } = await Promise.resolve().then(() => s3);
2637
+ return S3FilesystemProvider2;
2638
+ });
2639
+ registerProvider("gdrive", async () => {
2640
+ const { GoogleDriveProvider: GoogleDriveProvider2 } = await Promise.resolve().then(() => gdrive);
2641
+ return GoogleDriveProvider2;
2642
+ });
2643
+ }
2644
+ function isProviderAvailable(type) {
2645
+ return providers.has(type);
2646
+ }
2647
+ function getProviderInfo(type) {
2648
+ const descriptions = {
2649
+ local: "Local filesystem provider using Node.js fs module",
2650
+ s3: "S3-compatible provider supporting AWS S3, MinIO, and other S3-compatible services",
2651
+ gdrive: "Google Drive provider using Google Drive API v3",
2652
+ webdav: "WebDAV provider supporting Nextcloud, ownCloud, Apache mod_dav, and other WebDAV servers",
2653
+ "browser-storage": "Browser storage provider using IndexedDB for app file management"
2654
+ };
2655
+ const requiredOptions = {
2656
+ local: [],
2657
+ s3: ["region", "bucket"],
2658
+ gdrive: [],
2659
+ webdav: ["baseUrl", "username", "password"],
2660
+ "browser-storage": []
2661
+ };
2662
+ return {
2663
+ available: isProviderAvailable(type),
2664
+ description: descriptions[type] || "Unknown provider",
2665
+ requiredOptions: requiredOptions[type] || []
2666
+ };
2667
+ }
2668
+ const factory = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
2669
+ __proto__: null,
2670
+ getAvailableProviders,
2671
+ getFilesystem,
2672
+ getProviderInfo,
2673
+ initializeProviders,
2674
+ isProviderAvailable,
2675
+ registerProvider
2676
+ }, Symbol.toStringTag, { value: "Module" }));
2677
+ Promise.resolve().then(() => factory).then(({ initializeProviders: initializeProviders2 }) => {
2678
+ initializeProviders2().catch(() => {
2679
+ });
2680
+ });
2681
+ const PACKAGE_VERSION_INITIALIZED = true;
2682
+ export {
2683
+ DirectoryNotEmptyError,
2684
+ FileNotFoundError,
2685
+ FilesystemAdapter,
2686
+ FilesystemError,
2687
+ GoogleDriveProvider,
2688
+ InvalidPathError,
2689
+ LocalFilesystemProvider,
2690
+ PACKAGE_VERSION_INITIALIZED,
2691
+ PermissionError,
2692
+ S3FilesystemProvider,
2693
+ addRateLimit,
2694
+ factory as default,
2695
+ download,
2696
+ downloadFileWithCache,
2697
+ ensureDirectoryExists,
2698
+ fetchBuffer,
2699
+ fetchJSON,
2700
+ fetchText,
2701
+ fetchToFile,
2702
+ getAvailableProviders,
2703
+ getCached,
2704
+ getFilesystem,
2705
+ getMimeType,
2706
+ getProviderInfo,
2707
+ getRateLimit,
2708
+ initializeProviders,
2709
+ isDirectory,
2710
+ isFile,
2711
+ isProviderAvailable,
2712
+ listFiles,
2713
+ redactFilesystemConfig,
2714
+ registerProvider,
2715
+ setCached,
2716
+ upload,
2717
+ writeResponseToFile
2718
+ };
2719
+ //# sourceMappingURL=index.js.map