TypeScript · 7390 bytes Raw Blame History
1 import { v2 as webdav } from 'webdav-server';
2 import type { ZephyrFSClient } from '../integration/zephyrfs-client.js';
3 import { Readable } from 'node:stream';
4
5 export class ZephyrFSWebDAVFileSystem extends webdav.FileSystem {
6 private zephyrfs: ZephyrFSClient;
7
8 constructor(zephyrfsClient: ZephyrFSClient) {
9 super();
10 this.zephyrfs = zephyrfsClient;
11 }
12
13 _lockManager(): webdav.LockManager {
14 return new webdav.LocalLockManager();
15 }
16
17 _propertyManager(): webdav.PropertyManager {
18 return new webdav.LocalPropertyManager();
19 }
20
21 async _fastExistCheck(ctx: webdav.RequestContext, path: webdav.Path, callback: webdav.SimpleCallback): Promise<void> {
22 try {
23 if (path.isRoot()) {
24 return callback(null, true);
25 }
26
27 // Try to get file info to check if it exists
28 await this.zephyrfs.listFiles(path.toString());
29 callback(null, true);
30 } catch (error) {
31 callback(null, false);
32 }
33 }
34
35 async _type(ctx: webdav.RequestContext, path: webdav.Path, callback: webdav.SimpleCallback): Promise<void> {
36 try {
37 if (path.isRoot()) {
38 return callback(null, webdav.ResourceType.Directory);
39 }
40
41 const listing = await this.zephyrfs.listFiles(path.getParent().toString());
42 const fileName = path.fileName();
43 const file = listing.files.find(f => f.name === fileName);
44
45 if (!file) {
46 return callback(webdav.Errors.ResourceNotFound);
47 }
48
49 callback(null, file.type === 'directory' ? webdav.ResourceType.Directory : webdav.ResourceType.File);
50 } catch (error) {
51 callback(webdav.Errors.ResourceNotFound);
52 }
53 }
54
55 async _size(ctx: webdav.RequestContext, path: webdav.Path, callback: webdav.SimpleCallback): Promise<void> {
56 try {
57 const listing = await this.zephyrfs.listFiles(path.getParent().toString());
58 const fileName = path.fileName();
59 const file = listing.files.find(f => f.name === fileName);
60
61 if (!file) {
62 return callback(webdav.Errors.ResourceNotFound);
63 }
64
65 callback(null, file.size);
66 } catch (error) {
67 callback(webdav.Errors.ResourceNotFound);
68 }
69 }
70
71 async _lastModifiedDate(ctx: webdav.RequestContext, path: webdav.Path, callback: webdav.SimpleCallback): Promise<void> {
72 try {
73 const listing = await this.zephyrfs.listFiles(path.getParent().toString());
74 const fileName = path.fileName();
75 const file = listing.files.find(f => f.name === fileName);
76
77 if (!file) {
78 return callback(webdav.Errors.ResourceNotFound);
79 }
80
81 callback(null, file.lastModified.getTime());
82 } catch (error) {
83 callback(webdav.Errors.ResourceNotFound);
84 }
85 }
86
87 async _readDir(ctx: webdav.RequestContext, path: webdav.Path, callback: webdav.SimpleCallback): Promise<void> {
88 try {
89 const listing = await this.zephyrfs.listFiles(path.toString());
90 const children = listing.files.map(file => file.name);
91 callback(null, children);
92 } catch (error) {
93 callback(webdav.Errors.ResourceNotFound);
94 }
95 }
96
97 async _openReadStream(ctx: webdav.RequestContext, path: webdav.Path, info: webdav.OpenReadStreamInfo, callback: webdav.SimpleCallback): Promise<void> {
98 try {
99 // Get file info first to get the file ID
100 const listing = await this.zephyrfs.listFiles(path.getParent().toString());
101 const fileName = path.fileName();
102 const file = listing.files.find(f => f.name === fileName);
103
104 if (!file) {
105 return callback(webdav.Errors.ResourceNotFound);
106 }
107
108 const download = await this.zephyrfs.downloadFile(file.id);
109
110 // Convert web ReadableStream to Node.js Readable
111 const nodeStream = new Readable({
112 read() {} // No-op, we'll push data manually
113 });
114
115 const reader = download.stream.getReader();
116
117 const pump = async () => {
118 try {
119 while (true) {
120 const { done, value } = await reader.read();
121 if (done) {
122 nodeStream.push(null); // End stream
123 break;
124 }
125 nodeStream.push(Buffer.from(value));
126 }
127 } catch (error) {
128 nodeStream.destroy(error);
129 }
130 };
131
132 pump();
133
134 callback(null, nodeStream);
135 } catch (error) {
136 callback(webdav.Errors.ResourceNotFound);
137 }
138 }
139
140 async _openWriteStream(ctx: webdav.RequestContext, path: webdav.Path, info: webdav.OpenWriteStreamInfo, callback: webdav.SimpleCallback): Promise<void> {
141 try {
142 const chunks: Buffer[] = [];
143 const writeStream = new (require('stream').Writable)({
144 write(chunk: Buffer, encoding: string, callback: Function) {
145 chunks.push(chunk);
146 callback();
147 },
148 final: async (callback: Function) => {
149 try {
150 const data = Buffer.concat(chunks);
151 const fileName = path.fileName();
152 const parentPath = path.getParent().toString();
153
154 await this.zephyrfs.uploadFile(parentPath, fileName, data);
155 callback();
156 } catch (error) {
157 callback(error);
158 }
159 }
160 });
161
162 callback(null, writeStream);
163 } catch (error) {
164 callback(error);
165 }
166 }
167
168 async _delete(ctx: webdav.RequestContext, path: webdav.Path, callback: webdav.SimpleCallback): Promise<void> {
169 try {
170 // Get file info to get the file ID
171 const listing = await this.zephyrfs.listFiles(path.getParent().toString());
172 const fileName = path.fileName();
173 const file = listing.files.find(f => f.name === fileName);
174
175 if (!file) {
176 return callback(webdav.Errors.ResourceNotFound);
177 }
178
179 await this.zephyrfs.deleteFile(file.id);
180 callback(null);
181 } catch (error) {
182 callback(webdav.Errors.ResourceNotFound);
183 }
184 }
185
186 async _create(ctx: webdav.RequestContext, path: webdav.Path, type: webdav.ResourceType, callback: webdav.SimpleCallback): Promise<void> {
187 if (type === webdav.ResourceType.Directory) {
188 // Directories are created implicitly when files are uploaded to them
189 callback(null);
190 } else {
191 // Files are created when written to
192 callback(null);
193 }
194 }
195
196 async _move(ctx: webdav.RequestContext, pathFrom: webdav.Path, pathTo: webdav.Path, callback: webdav.SimpleCallback): Promise<void> {
197 // Move/rename not implemented in basic version
198 callback(webdav.Errors.MethodNotAllowed);
199 }
200
201 async _copy(ctx: webdav.RequestContext, pathFrom: webdav.Path, pathTo: webdav.Path, callback: webdav.SimpleCallback): Promise<void> {
202 // Copy not implemented in basic version
203 callback(webdav.Errors.MethodNotAllowed);
204 }
205 }
206
207 export function createWebDAVServer(zephyrfsClient: ZephyrFSClient, options: {
208 port?: number;
209 host?: string;
210 username?: string;
211 password?: string;
212 } = {}) {
213 const server = new webdav.WebDAVServer({
214 port: options.port || 3001,
215 hostname: options.host || '0.0.0.0',
216 });
217
218 // Add authentication if provided
219 if (options.username && options.password) {
220 const userManager = new webdav.SimpleUserManager();
221 const user = userManager.addUser(options.username, options.password);
222
223 server.setDefaultUser(user);
224 server.setHTTPAuthentication(new webdav.HTTPBasicAuthentication(userManager));
225 }
226
227 // Set up the file system
228 server.setFileSystem('/', new ZephyrFSWebDAVFileSystem(zephyrfsClient));
229
230 return server;
231 }