@cmdoss/file-manager 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -0
- package/package.json +8 -3
- package/.turbo/turbo-build.log +0 -14
- package/dist/index.d.ts +0 -24
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/src/file-manager.ts +0 -166
- package/src/index.ts +0 -1
- package/tsconfig.json +0 -11
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# File Manager
|
|
2
|
+
|
|
3
|
+
A cross-platform file system abstraction for the CommandOSS ts-sdks, providing unified file operations in both Node.js and browser environments using [zenfs](https://github.com/zen-fs/core).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🌐 **Universal**: Works in Node.js and browser environments seamlessly
|
|
8
|
+
- 📁 **Virtual FileSystem**: ZenFS-based in-memory file system
|
|
9
|
+
- 🔒 **Type-Safe**: Full TypeScript support with comprehensive interfaces
|
|
10
|
+
- 🚀 **Efficient**: Lazy-loaded, memory-efficient file operations
|
|
11
|
+
- 🎯 **Abstracted**: Simple interface for common file operations
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @cmdoss/file-manager
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { ZenFsFileManager } from '@cmdoss/file-manager'
|
|
23
|
+
|
|
24
|
+
const fileManager = new ZenFsFileManager()
|
|
25
|
+
|
|
26
|
+
// Read a file
|
|
27
|
+
const content = await fileManager.readFile('/path/to/file.txt')
|
|
28
|
+
|
|
29
|
+
// Write a file
|
|
30
|
+
await fileManager.writeFile('/path/to/output.txt', 'content')
|
|
31
|
+
|
|
32
|
+
// List directory contents
|
|
33
|
+
const files = await fileManager.listDirectory('/path/to/dir')
|
|
34
|
+
|
|
35
|
+
// Check if file exists
|
|
36
|
+
const exists = await fileManager.fileExists('/path/to/file.txt')
|
|
37
|
+
|
|
38
|
+
// Get file metadata
|
|
39
|
+
const stat = await fileManager.stat('/path/to/file.txt')
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
### IFileManager Interface
|
|
45
|
+
|
|
46
|
+
All operations are defined by the `IFileManager` interface:
|
|
47
|
+
|
|
48
|
+
- `readFile(path: string): Promise<Buffer>` - Read file contents
|
|
49
|
+
- `writeFile(path: string, content: Buffer | string): Promise<void>` - Write file contents
|
|
50
|
+
- `listDirectory(path: string): Promise<string[]>` - List directory contents
|
|
51
|
+
- `fileExists(path: string): Promise<boolean>` - Check if file exists
|
|
52
|
+
- `stat(path: string): Promise<Stats>` - Get file metadata
|
|
53
|
+
- `mkdir(path: string): Promise<void>` - Create directory
|
|
54
|
+
- `rm(path: string): Promise<void>` - Delete file or directory
|
|
55
|
+
- `getWorkspacePath(): string` - Get current workspace directory
|
|
56
|
+
|
|
57
|
+
## Workspace Concept
|
|
58
|
+
|
|
59
|
+
The file manager tracks a workspace directory boundary within the virtual filesystem. This is useful for:
|
|
60
|
+
|
|
61
|
+
- Organizing site resources in a contained directory
|
|
62
|
+
- Computing relative paths for deployment
|
|
63
|
+
- Isolating operations to a specific project folder
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const fileManager = new ZenFsFileManager('/workspace')
|
|
67
|
+
|
|
68
|
+
// Operations are relative to workspace
|
|
69
|
+
await fileManager.writeFile('index.html', '<html>...</html>')
|
|
70
|
+
// Actually written to /workspace/index.html
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Architecture
|
|
74
|
+
|
|
75
|
+
- **ZenFsFileManager**: Concrete implementation using zenfs virtual filesystem
|
|
76
|
+
- **IFileManager**: Interface for cross-platform file operations
|
|
77
|
+
- Supports both Node.js `fs` module and browser File API backends
|
|
78
|
+
|
|
79
|
+
## Usage with Site Builder
|
|
80
|
+
|
|
81
|
+
The file manager is typically used with the site-builder SDK:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { WalrusSiteBuilderSdk } from '@cmdoss/site-builder'
|
|
85
|
+
import { ZenFsFileManager } from '@cmdoss/file-manager'
|
|
86
|
+
|
|
87
|
+
const fileManager = new ZenFsFileManager('/my-site')
|
|
88
|
+
const sdk = new WalrusSiteBuilderSdk({
|
|
89
|
+
fileManager,
|
|
90
|
+
// ... other options
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// SDK uses file manager for reading/writing site resources
|
|
94
|
+
await sdk.publishSite()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Browser vs Node.js
|
|
98
|
+
|
|
99
|
+
The file manager automatically adapts to the environment:
|
|
100
|
+
|
|
101
|
+
- **Node.js**: Uses native `fs` module for actual file operations
|
|
102
|
+
- **Browser**: Uses in-memory virtual filesystem with zenfs
|
|
103
|
+
|
|
104
|
+
This allows the same code to work in both environments without modification.
|
|
105
|
+
|
|
106
|
+
## Contributing
|
|
107
|
+
|
|
108
|
+
1. Implement the `IFileManager` interface for new backends
|
|
109
|
+
2. Add tests for cross-platform compatibility
|
|
110
|
+
3. Ensure all operations work in both Node.js and browser
|
|
111
|
+
4. Submit a pull request
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
116
|
+
|
|
117
|
+
## See Also
|
|
118
|
+
|
|
119
|
+
- [Site Builder SDK](../site-builder/) - Main SDK using this file manager
|
|
120
|
+
- [zenfs](https://github.com/zen-fs/core) - Virtual filesystem library
|
|
121
|
+
- [AGENTS.md](../../AGENTS.md) - Detailed project architecture
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmdoss/file-manager",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./dist/index.js",
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"access": "public",
|
|
10
10
|
"tag": "latest"
|
|
11
11
|
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
12
15
|
"peerDependencies": {
|
|
13
16
|
"@zenfs/archives": "^1.3.1",
|
|
14
17
|
"@zenfs/core": "^2.4.2",
|
|
@@ -20,15 +23,17 @@
|
|
|
20
23
|
"@zenfs/core": "^2.4.4",
|
|
21
24
|
"@zenfs/dom": "^1.2.5",
|
|
22
25
|
"debug": "^4.4.3",
|
|
23
|
-
"@cmdoss/site-builder": "0.2.
|
|
26
|
+
"@cmdoss/site-builder": "0.2.3"
|
|
24
27
|
},
|
|
25
28
|
"devDependencies": {
|
|
26
29
|
"@types/node": "^22.19.1",
|
|
27
|
-
"tsdown": "^0.
|
|
30
|
+
"tsdown": "^0.17.3"
|
|
28
31
|
},
|
|
29
32
|
"scripts": {
|
|
30
33
|
"dev": "tsdown --platform browser --watch --ignore-watch .turbo --no-clean",
|
|
31
34
|
"test": "node --test",
|
|
35
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-exclude='src/**/*.test.ts'",
|
|
36
|
+
"test:watch": "node --test --watch",
|
|
32
37
|
"check:types": "tsc --noEmit",
|
|
33
38
|
"build": "tsdown --platform browser --minify"
|
|
34
39
|
}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @cmdoss/file-manager@0.2.2 build /home/runner/work/ts-sdks/ts-sdks/packages/file-manager
|
|
3
|
-
> tsdown --platform browser --minify
|
|
4
|
-
|
|
5
|
-
[34mℹ[39m tsdown [2mv0.16.8[22m powered by rolldown [2mv1.0.0-beta.52[22m
|
|
6
|
-
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
7
|
-
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
8
|
-
[34mℹ[39m Build start
|
|
9
|
-
[34mℹ[39m [2mdist/[22m[1mindex.js[22m [2m3.01 kB[22m [2m│ gzip: 1.24 kB[22m
|
|
10
|
-
[34mℹ[39m [2mdist/[22mindex.js.map [2m8.42 kB[22m [2m│ gzip: 2.79 kB[22m
|
|
11
|
-
[34mℹ[39m [2mdist/[22mindex.d.ts.map [2m0.87 kB[22m [2m│ gzip: 0.37 kB[22m
|
|
12
|
-
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.ts[22m[39m [2m1.06 kB[22m [2m│ gzip: 0.50 kB[22m
|
|
13
|
-
[34mℹ[39m 4 files, total: 13.36 kB
|
|
14
|
-
[32m✔[39m Build complete in [32m268ms[39m
|
package/dist/index.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { FileChangedCallback, IFileManager } from "@cmdoss/site-builder";
|
|
2
|
-
|
|
3
|
-
//#region src/file-manager.d.ts
|
|
4
|
-
declare class ZenFsFileManager implements IFileManager {
|
|
5
|
-
/** The directory of the workspace. Any files within this directory are considered part of the workspace. */
|
|
6
|
-
readonly workspaceDir: string;
|
|
7
|
-
/** The directory where the workspace is mounted in the virtual filesystem. */
|
|
8
|
-
readonly mountDir?: string | undefined;
|
|
9
|
-
protected changeListeners: Set<FileChangedCallback>;
|
|
10
|
-
constructor(workspaceDir?: string, mountDir?: string | undefined);
|
|
11
|
-
initialize(): Promise<void>;
|
|
12
|
-
writeZipArchive(zipData: ArrayBuffer): Promise<void>;
|
|
13
|
-
readFile(filePath: string): Promise<Uint8Array>;
|
|
14
|
-
listFiles(): Promise<string[]>;
|
|
15
|
-
getSize(): Promise<number>;
|
|
16
|
-
writeFile(filePath: string, content: string | Uint8Array): Promise<void>;
|
|
17
|
-
deleteFile(filePath: string): Promise<void>;
|
|
18
|
-
onFileChange(callback: FileChangedCallback): () => void;
|
|
19
|
-
private notifyChange;
|
|
20
|
-
unmount(): void;
|
|
21
|
-
}
|
|
22
|
-
//#endregion
|
|
23
|
-
export { ZenFsFileManager };
|
|
24
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":["mountDir?: string"],"sources":["../src/file-manager.ts"],"sourcesContent":[],"mappings":";;;cASa,gBAAA,YAA4B;;EAAzC,SAAa,YAAA,EAAA,MAAA;EACoB;EAAJ,SAAA,QAAA,CAAA,EAAA,MAAA,GAAA,SAAA;EAyBP,UAAA,eAAA,EAzBO,GAyBP,CAzBW,mBAyBX,CAAA;EAcW,WAAA,CAAA,YAAA,CAAA,EAAA,MAAA,EAAA,QAAA,CAAA,EAAA,MAAA,GAAA,SAAA;EAAc,UAAA,CAAA,CAAA,EAdzB,OAcyB,CAAA,IAAA,CAAA;EAmBH,eAAA,CAAA,OAAA,EAnBX,WAmBW,CAAA,EAnBG,OAmBH,CAAA,IAAA,CAAA;EAAR,QAAA,CAAA,QAAA,EAAA,MAAA,CAAA,EAAA,OAAA,CAAQ,UAAR,CAAA;EASf,SAAA,CAAA,CAAA,EAAA,OAAA,CAAA,MAAA,EAAA,CAAA;EAcF,OAAA,CAAA,CAAA,EAAA,OAAA,CAAA,MAAA,CAAA;EAwBG,SAAA,CAAA,QAAA,EAAA,MAAA,EAAA,OAAA,EAAA,MAAA,GAAA,UAAA,CAAA,EACjB,OADiB,CAAA,IAAA,CAAA;EACjB,UAAA,CAAA,QAAA,EAAA,MAAA,CAAA,EAaiC,OAbjC,CAAA,IAAA,CAAA;EAaiC,YAAA,CAAA,QAAA,EASb,mBATa,CAAA,EAAA,GAAA,GAAA,IAAA;EASb,QAAA,YAAA;EAjIgB,OAAA,CAAA,CAAA,EAAA,IAAA"}
|
package/dist/index.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{Zip as e}from"@zenfs/archives";import{configure as t,fs as n}from"@zenfs/core";import*as r from"@zenfs/core/path";import{IndexedDB as i}from"@zenfs/dom";import a from"debug";const o=a(`file-manager`);var s=class{changeListeners=new Set;constructor(e=`/workspace`,t){if(this.workspaceDir=e,this.mountDir=t,this.mountDir||=this.workspaceDir,this.mountDir!==this.workspaceDir){let e=r.normalize(this.workspaceDir),t=r.normalize(this.mountDir);if(!e.startsWith(t+`/`)&&e!==t)throw Error(`workspaceDir (${this.workspaceDir}) must be a subdirectory of mountDir (${this.mountDir})`)}}async initialize(){o(`🔧 Configuring filesystem...`),o(`📁 Mounting workspace at ${this.mountDir}`),await t({mounts:{[this.mountDir??this.workspaceDir]:{backend:i,storeName:this.mountDir??this.workspaceDir}}}),o(`✅ Filesystem configured`)}async writeZipArchive(r){let i=`/tmp/zip-write-${crypto.randomUUID()}`;o(`📦 Mounting ZIP archive to temporary directory...`),await t({mounts:{[i]:{backend:e,data:r,lazy:!1}}}),o(`📂 Copying files from ZIP archive to workspace...`),await n.promises.cp(i,this.workspaceDir,{recursive:!0}),o(`📦 Unmounting temporary ZIP directory...`),n.umount(i),o(`✅ Files copied to workspace from ZIP archive`)}async readFile(e){o(`📂 Reading file ${e}...`),e=c(e);let t=r.join(this.workspaceDir,e),i=await n.promises.readFile(t);return o(`✅ File ${e} read (${i.byteLength} bytes)`),i}async listFiles(){o(`📄 Listing files in workspace`);let e=(await n.promises.readdir(this.workspaceDir,{withFileTypes:!0,recursive:!0})).filter(e=>e.isFile()).map(e=>r.join(e.parentPath,e.name)).map(c);return o(`✅ Files currently in workspace:`,e),e}async getSize(){o(`📏 Calculating total size of files in workspace`);let e=0,t=await n.promises.readdir(this.workspaceDir,{withFileTypes:!0,recursive:!0});for(let i of t)if(i.isFile()){let t=r.join(this.workspaceDir,i.parentPath,i.name),a=await n.promises.stat(t);e+=a.size}return o(`✅ Total size of files in workspace:`,e,`bytes`),e}async writeFile(e,t){o(`✍️ Writing file to`,e),e=c(e);let i=r.join(this.workspaceDir,e),a=r.dirname(i);await n.promises.mkdir(a,{recursive:!0});let s=typeof t==`string`?new TextEncoder().encode(t):t;await n.promises.writeFile(i,s),o(`✅ File written to`,e,`(`,s.byteLength,`bytes )`),this.notifyChange({type:`updated`,path:e})}async deleteFile(e){o(`🗑️ Deleting file at`,e),e=c(e);let t=r.join(this.workspaceDir,e);await n.promises.rm(t),o(`✅ File deleted at`,e),this.notifyChange({type:`removed`,path:e})}onFileChange(e){return this.changeListeners.add(e),()=>{this.changeListeners.delete(e)}}notifyChange(e){for(let t of this.changeListeners)t(e)}unmount(){o(`🚪 Unmounting workspace directory (`,this.workspaceDir,`)`),this.changeListeners.clear(),this.mountDir===this.workspaceDir?n.umount(this.mountDir):o(`⚠️ Skipping unmount: mountDir and workspaceDir are different`)}};function c(e){return e.startsWith(`/`)?e:`/${e}`}export{s as ZenFsFileManager};
|
|
2
|
-
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["mountDir?: string","path"],"sources":["../src/file-manager.ts"],"sourcesContent":["import type { FileChangedCallback, IFileManager } from '@cmdoss/site-builder'\nimport { Zip } from '@zenfs/archives'\nimport { configure, fs } from '@zenfs/core'\nimport * as path from '@zenfs/core/path'\nimport { IndexedDB } from '@zenfs/dom'\nimport debug from 'debug'\n\nconst log = debug('file-manager')\n\nexport class ZenFsFileManager implements IFileManager {\n protected changeListeners: Set<FileChangedCallback> = new Set()\n constructor(\n /** The directory of the workspace. Any files within this directory are considered part of the workspace. */\n public readonly workspaceDir = '/workspace',\n /** The directory where the workspace is mounted in the virtual filesystem. */\n public readonly mountDir?: string\n ) {\n if (!this.mountDir) {\n this.mountDir = this.workspaceDir\n }\n if (this.mountDir !== this.workspaceDir) {\n // Ensure workspaceDir is a subdirectory of mountDir\n const normalizedWorkspace = path.normalize(this.workspaceDir)\n const normalizedMount = path.normalize(this.mountDir)\n if (\n !normalizedWorkspace.startsWith(normalizedMount + '/') &&\n normalizedWorkspace !== normalizedMount\n ) {\n throw new Error(\n `workspaceDir (${this.workspaceDir}) must be a subdirectory of mountDir (${this.mountDir})`\n )\n }\n }\n }\n\n async initialize(): Promise<void> {\n log('🔧 Configuring filesystem...')\n log(`📁 Mounting workspace at ${this.mountDir}`)\n await configure({\n mounts: {\n [this.mountDir ?? this.workspaceDir]: {\n backend: IndexedDB,\n storeName: this.mountDir ?? this.workspaceDir\n }\n }\n })\n log('✅ Filesystem configured')\n }\n\n async writeZipArchive(zipData: ArrayBuffer): Promise<void> {\n const tmpDir = `/tmp/zip-write-${crypto.randomUUID()}`\n log('📦 Mounting ZIP archive to temporary directory...')\n await configure({\n mounts: {\n [tmpDir]: {\n backend: Zip,\n data: zipData,\n lazy: false // Extract all files immediately\n }\n }\n })\n log('📂 Copying files from ZIP archive to workspace...')\n await fs.promises.cp(tmpDir, this.workspaceDir, { recursive: true })\n log('📦 Unmounting temporary ZIP directory...')\n fs.umount(tmpDir)\n log('✅ Files copied to workspace from ZIP archive')\n }\n\n async readFile(filePath: string): Promise<Uint8Array> {\n log(`📂 Reading file ${filePath}...`)\n filePath = ensureLeadingSlash(filePath)\n const workspaceFilePath = path.join(this.workspaceDir, filePath)\n const content = await fs.promises.readFile(workspaceFilePath)\n log(`✅ File ${filePath} read (${content.byteLength} bytes)`)\n return content\n }\n\n async listFiles(): Promise<string[]> {\n log('📄 Listing files in workspace')\n const files = await fs.promises.readdir(this.workspaceDir, {\n withFileTypes: true,\n recursive: true\n })\n const result = files\n .filter(f => f.isFile())\n .map(f => path.join(f.parentPath, f.name))\n .map(ensureLeadingSlash)\n log('✅ Files currently in workspace:', result)\n return result\n }\n\n async getSize(): Promise<number> {\n log('📏 Calculating total size of files in workspace')\n let totalSize = 0\n const files = await fs.promises.readdir(this.workspaceDir, {\n withFileTypes: true,\n recursive: true\n })\n for (const file of files) {\n if (file.isFile()) {\n const filePath = path.join(\n this.workspaceDir,\n file.parentPath,\n file.name\n )\n const stats = await fs.promises.stat(filePath)\n totalSize += stats.size\n }\n }\n log('✅ Total size of files in workspace:', totalSize, 'bytes')\n return totalSize\n }\n\n async writeFile(\n filePath: string,\n content: string | Uint8Array\n ): Promise<void> {\n log('✍️ Writing file to', filePath)\n filePath = ensureLeadingSlash(filePath)\n const workspaceFilePath = path.join(this.workspaceDir, filePath)\n const dir = path.dirname(workspaceFilePath)\n await fs.promises.mkdir(dir, { recursive: true })\n const contentBytes =\n typeof content === 'string' ? new TextEncoder().encode(content) : content\n await fs.promises.writeFile(workspaceFilePath, contentBytes)\n log('✅ File written to', filePath, '(', contentBytes.byteLength, 'bytes )')\n this.notifyChange({ type: 'updated', path: filePath })\n }\n\n async deleteFile(filePath: string): Promise<void> {\n log('🗑️ Deleting file at', filePath)\n filePath = ensureLeadingSlash(filePath)\n const workspaceFilePath = path.join(this.workspaceDir, filePath)\n await fs.promises.rm(workspaceFilePath)\n log('✅ File deleted at', filePath)\n this.notifyChange({ type: 'removed', path: filePath })\n }\n\n onFileChange(callback: FileChangedCallback): () => void {\n this.changeListeners.add(callback)\n return () => {\n this.changeListeners.delete(callback)\n }\n }\n\n private notifyChange(arg: Parameters<FileChangedCallback>[0]): void {\n for (const listener of this.changeListeners) {\n listener(arg)\n }\n }\n\n unmount(): void {\n log('🚪 Unmounting workspace directory (', this.workspaceDir, ')')\n this.changeListeners.clear()\n if (this.mountDir === this.workspaceDir) {\n // Unmount only if mountDir and workspaceDir are the same\n fs.umount(this.mountDir)\n } else {\n log('⚠️ Skipping unmount: mountDir and workspaceDir are different')\n }\n }\n}\n\nfunction ensureLeadingSlash(path: string): string {\n return path.startsWith('/') ? path : `/${path}`\n}\n"],"mappings":"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"}
|
package/src/file-manager.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import type { FileChangedCallback, IFileManager } from '@cmdoss/site-builder'
|
|
2
|
-
import { Zip } from '@zenfs/archives'
|
|
3
|
-
import { configure, fs } from '@zenfs/core'
|
|
4
|
-
import * as path from '@zenfs/core/path'
|
|
5
|
-
import { IndexedDB } from '@zenfs/dom'
|
|
6
|
-
import debug from 'debug'
|
|
7
|
-
|
|
8
|
-
const log = debug('file-manager')
|
|
9
|
-
|
|
10
|
-
export class ZenFsFileManager implements IFileManager {
|
|
11
|
-
protected changeListeners: Set<FileChangedCallback> = new Set()
|
|
12
|
-
constructor(
|
|
13
|
-
/** The directory of the workspace. Any files within this directory are considered part of the workspace. */
|
|
14
|
-
public readonly workspaceDir = '/workspace',
|
|
15
|
-
/** The directory where the workspace is mounted in the virtual filesystem. */
|
|
16
|
-
public readonly mountDir?: string
|
|
17
|
-
) {
|
|
18
|
-
if (!this.mountDir) {
|
|
19
|
-
this.mountDir = this.workspaceDir
|
|
20
|
-
}
|
|
21
|
-
if (this.mountDir !== this.workspaceDir) {
|
|
22
|
-
// Ensure workspaceDir is a subdirectory of mountDir
|
|
23
|
-
const normalizedWorkspace = path.normalize(this.workspaceDir)
|
|
24
|
-
const normalizedMount = path.normalize(this.mountDir)
|
|
25
|
-
if (
|
|
26
|
-
!normalizedWorkspace.startsWith(normalizedMount + '/') &&
|
|
27
|
-
normalizedWorkspace !== normalizedMount
|
|
28
|
-
) {
|
|
29
|
-
throw new Error(
|
|
30
|
-
`workspaceDir (${this.workspaceDir}) must be a subdirectory of mountDir (${this.mountDir})`
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async initialize(): Promise<void> {
|
|
37
|
-
log('🔧 Configuring filesystem...')
|
|
38
|
-
log(`📁 Mounting workspace at ${this.mountDir}`)
|
|
39
|
-
await configure({
|
|
40
|
-
mounts: {
|
|
41
|
-
[this.mountDir ?? this.workspaceDir]: {
|
|
42
|
-
backend: IndexedDB,
|
|
43
|
-
storeName: this.mountDir ?? this.workspaceDir
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
log('✅ Filesystem configured')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async writeZipArchive(zipData: ArrayBuffer): Promise<void> {
|
|
51
|
-
const tmpDir = `/tmp/zip-write-${crypto.randomUUID()}`
|
|
52
|
-
log('📦 Mounting ZIP archive to temporary directory...')
|
|
53
|
-
await configure({
|
|
54
|
-
mounts: {
|
|
55
|
-
[tmpDir]: {
|
|
56
|
-
backend: Zip,
|
|
57
|
-
data: zipData,
|
|
58
|
-
lazy: false // Extract all files immediately
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
log('📂 Copying files from ZIP archive to workspace...')
|
|
63
|
-
await fs.promises.cp(tmpDir, this.workspaceDir, { recursive: true })
|
|
64
|
-
log('📦 Unmounting temporary ZIP directory...')
|
|
65
|
-
fs.umount(tmpDir)
|
|
66
|
-
log('✅ Files copied to workspace from ZIP archive')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async readFile(filePath: string): Promise<Uint8Array> {
|
|
70
|
-
log(`📂 Reading file ${filePath}...`)
|
|
71
|
-
filePath = ensureLeadingSlash(filePath)
|
|
72
|
-
const workspaceFilePath = path.join(this.workspaceDir, filePath)
|
|
73
|
-
const content = await fs.promises.readFile(workspaceFilePath)
|
|
74
|
-
log(`✅ File ${filePath} read (${content.byteLength} bytes)`)
|
|
75
|
-
return content
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async listFiles(): Promise<string[]> {
|
|
79
|
-
log('📄 Listing files in workspace')
|
|
80
|
-
const files = await fs.promises.readdir(this.workspaceDir, {
|
|
81
|
-
withFileTypes: true,
|
|
82
|
-
recursive: true
|
|
83
|
-
})
|
|
84
|
-
const result = files
|
|
85
|
-
.filter(f => f.isFile())
|
|
86
|
-
.map(f => path.join(f.parentPath, f.name))
|
|
87
|
-
.map(ensureLeadingSlash)
|
|
88
|
-
log('✅ Files currently in workspace:', result)
|
|
89
|
-
return result
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async getSize(): Promise<number> {
|
|
93
|
-
log('📏 Calculating total size of files in workspace')
|
|
94
|
-
let totalSize = 0
|
|
95
|
-
const files = await fs.promises.readdir(this.workspaceDir, {
|
|
96
|
-
withFileTypes: true,
|
|
97
|
-
recursive: true
|
|
98
|
-
})
|
|
99
|
-
for (const file of files) {
|
|
100
|
-
if (file.isFile()) {
|
|
101
|
-
const filePath = path.join(
|
|
102
|
-
this.workspaceDir,
|
|
103
|
-
file.parentPath,
|
|
104
|
-
file.name
|
|
105
|
-
)
|
|
106
|
-
const stats = await fs.promises.stat(filePath)
|
|
107
|
-
totalSize += stats.size
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
log('✅ Total size of files in workspace:', totalSize, 'bytes')
|
|
111
|
-
return totalSize
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async writeFile(
|
|
115
|
-
filePath: string,
|
|
116
|
-
content: string | Uint8Array
|
|
117
|
-
): Promise<void> {
|
|
118
|
-
log('✍️ Writing file to', filePath)
|
|
119
|
-
filePath = ensureLeadingSlash(filePath)
|
|
120
|
-
const workspaceFilePath = path.join(this.workspaceDir, filePath)
|
|
121
|
-
const dir = path.dirname(workspaceFilePath)
|
|
122
|
-
await fs.promises.mkdir(dir, { recursive: true })
|
|
123
|
-
const contentBytes =
|
|
124
|
-
typeof content === 'string' ? new TextEncoder().encode(content) : content
|
|
125
|
-
await fs.promises.writeFile(workspaceFilePath, contentBytes)
|
|
126
|
-
log('✅ File written to', filePath, '(', contentBytes.byteLength, 'bytes )')
|
|
127
|
-
this.notifyChange({ type: 'updated', path: filePath })
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async deleteFile(filePath: string): Promise<void> {
|
|
131
|
-
log('🗑️ Deleting file at', filePath)
|
|
132
|
-
filePath = ensureLeadingSlash(filePath)
|
|
133
|
-
const workspaceFilePath = path.join(this.workspaceDir, filePath)
|
|
134
|
-
await fs.promises.rm(workspaceFilePath)
|
|
135
|
-
log('✅ File deleted at', filePath)
|
|
136
|
-
this.notifyChange({ type: 'removed', path: filePath })
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
onFileChange(callback: FileChangedCallback): () => void {
|
|
140
|
-
this.changeListeners.add(callback)
|
|
141
|
-
return () => {
|
|
142
|
-
this.changeListeners.delete(callback)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
private notifyChange(arg: Parameters<FileChangedCallback>[0]): void {
|
|
147
|
-
for (const listener of this.changeListeners) {
|
|
148
|
-
listener(arg)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
unmount(): void {
|
|
153
|
-
log('🚪 Unmounting workspace directory (', this.workspaceDir, ')')
|
|
154
|
-
this.changeListeners.clear()
|
|
155
|
-
if (this.mountDir === this.workspaceDir) {
|
|
156
|
-
// Unmount only if mountDir and workspaceDir are the same
|
|
157
|
-
fs.umount(this.mountDir)
|
|
158
|
-
} else {
|
|
159
|
-
log('⚠️ Skipping unmount: mountDir and workspaceDir are different')
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function ensureLeadingSlash(path: string): string {
|
|
165
|
-
return path.startsWith('/') ? path : `/${path}`
|
|
166
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './file-manager'
|
package/tsconfig.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
-
"extends": "../../configs/tsconfig.library.json",
|
|
4
|
-
"compilerOptions": {
|
|
5
|
-
"lib": ["ES2021", "DOM"],
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"emitDeclarationOnly": true,
|
|
8
|
-
"allowImportingTsExtensions": true
|
|
9
|
-
},
|
|
10
|
-
"include": ["src/**/*"]
|
|
11
|
-
}
|