Skip to content
File

Blob: src/workerd/api/filesystem.c++

114.5 KB
1#include "filesystem.h"
2 
3#include "blob.h"
4#include "url-standard.h"
5#include "url.h"
6 
7#include <workerd/api/node/exceptions.h>
8#include <workerd/api/streams/standard.h>
9#include <workerd/io/features.h>
10#include <workerd/jsg/setup.h>
11 
12namespace workerd::api {
13 
14// =======================================================================================
15// Implementation of cloudflare-internal:filesystem in support of node:fs
16 
17namespace {
18static constexpr uint32_t kMax = kj::maxValue;
19constexpr kj::StringPtr nameForFsType(FsType type) {
20 switch (type) {
21 case FsType::FILE:
22 return "file"_kj;
23 case FsType::DIRECTORY:
24 return "directory"_kj;
25 case FsType::SYMLINK:
26 return "symlink"_kj;
27 }
28 KJ_UNREACHABLE;
29}
30 
31// A file path is passed to the C++ layer as a URL object. However, we have two different
32// implementations of URL in the system. This class wraps and abstracts over both of them.
33struct NormalizedFilePath {
34 const jsg::Url url;
35 
36 static jsg::Url normalize(FileSystemModule::FilePath path) {
37 KJ_SWITCH_ONEOF(path) {
38 KJ_CASE_ONEOF(legacy, jsg::Ref<URL>) {
39 auto parsed = JSG_REQUIRE_NONNULL(
40 jsg::Url::tryParse(legacy->getHref(), "file:///"_kj), Error, "Invalid URL"_kj);
41 // The cloning here is necessary to de-percent-encode characters in the
42 // path that don't need to be percent-encoded, allowing us to treat equivalent
43 // encodings of the same path as equal. For instance, '/foo' and '/%66oo' should
44 // be considered the same path since 'f' and '%66' are equivalent. Importantly,
45 // this retains percent-encoding on characters that do need to be percent-encoded
46 // to be valid in URLs, such as non-ASCII characters.
47 return parsed.clone(jsg::Url::EquivalenceOption::NORMALIZE_PATH);
48 }
49 KJ_CASE_ONEOF(standard, jsg::Ref<url::URL>) {
50 jsg::Url url = *standard;
51 return url.clone(jsg::Url::EquivalenceOption::NORMALIZE_PATH);
52 }
53 }
54 KJ_UNREACHABLE;
55 }
56 
57 NormalizedFilePath(FileSystemModule::FilePath path): url(normalize(kj::mv(path))) {
58 validate();
59 }
60 
61 void validate() {
62 JSG_REQUIRE(url.getProtocol() == "file:"_kj, TypeError, "File path must be a file: URL");
63 JSG_REQUIRE(url.getHost().size() == 0, Error, "File path must not have a host");
64 
65 // Let's count the number of path segments in the URL. We want to make sure that
66 // it does not have more than 48 segments. Why 48? Great question! It's a completely
67 // arbitrary limit that we set to prevent excessively long paths that could lead to
68 // performance issues.
69 // We also limit the maximum total length of the path to 4096 characters.
70 auto pathname = url.getPathname();
71 JSG_REQUIRE(pathname.size() <= 4096, Error, "File path is too long"_kj);
72 
73 uint32_t segmentCount = 0;
74 for (auto i: kj::indices(pathname)) {
75 if (pathname[i] == '/') {
76 segmentCount++;
77 }
78 }
79 JSG_REQUIRE(segmentCount <= 48, Error, "File path has too many segments"_kj);
80 }
81 
82 operator const jsg::Url&() const {
83 return url;
84 }
85 
86 operator kj::Path() const {
87 auto path = kj::str(url.getPathname().slice(1));
88 kj::Path root{};
89 return root.eval(path);
90 }
91};
92 
93[[noreturn]] void throwFsError(
94 jsg::Lock& js, workerd::FsError error, kj::StringPtr syscall, kj::StringPtr path = nullptr) {
95 switch (error) {
96 case workerd::FsError::NOT_DIRECTORY: {
97 node::THROW_ERR_UV_ENOTDIR(js, syscall, nullptr, path);
98 }
99 case workerd::FsError::NOT_EMPTY: {
100 node::THROW_ERR_UV_ENOTEMPTY(js, syscall, nullptr, path);
101 }
102 case workerd::FsError::READ_ONLY: {
103 node::THROW_ERR_UV_EPERM(js, syscall, nullptr, path);
104 }
105 case workerd::FsError::TOO_MANY_OPEN_FILES: {
106 node::THROW_ERR_UV_EMFILE(js, syscall, nullptr, path);
107 }
108 case workerd::FsError::ALREADY_EXISTS: {
109 node::THROW_ERR_UV_EEXIST(js, syscall, nullptr, path);
110 }
111 case workerd::FsError::NOT_SUPPORTED: {
112 node::THROW_ERR_UV_ENOSYS(js, syscall, nullptr, path);
113 }
114 case workerd::FsError::NOT_PERMITTED: {
115 node::THROW_ERR_UV_EPERM(js, syscall, nullptr, path);
116 }
117 case workerd::FsError::NOT_PERMITTED_ON_DIRECTORY: {
118 node::THROW_ERR_UV_EISDIR(js, syscall, nullptr, path);
119 }
120 case workerd::FsError::FAILED: {
121 node::THROW_ERR_UV_EIO(js, syscall, nullptr, path);
122 }
123 case workerd::FsError::INVALID_PATH: {
124 node::THROW_ERR_UV_EINVAL(js, syscall, "Invalid path"_kj, path);
125 }
126 case workerd::FsError::FILE_SIZE_LIMIT_EXCEEDED: {
127 node::THROW_ERR_UV_EPERM(js, syscall, "File size limit exceeded"_kj, path);
128 }
129 case workerd::FsError::SYMLINK_DEPTH_EXCEEDED: {
130 node::THROW_ERR_UV_ELOOP(js, syscall, "symlink depth exceeded"_kj, path);
131 }
132 default: {
133 node::THROW_ERR_UV_EPERM(js, syscall, nullptr, path);
134 }
135 }
136 KJ_UNREACHABLE;
137}
138} // namespace
139 
140Stat::Stat(const workerd::Stat& stat)
141 : type(nameForFsType(stat.type)),
142 size(stat.size),
143 lastModified((stat.lastModified - kj::UNIX_EPOCH) / kj::NANOSECONDS),
144 created((stat.created - kj::UNIX_EPOCH) / kj::NANOSECONDS),
145 writable(stat.writable),
146 device(stat.device) {}
147 
148kj::Maybe<Stat> FileSystemModule::stat(
149 jsg::Lock& js, kj::OneOf<int, FilePath> pathOrFd, StatOptions options) {
150 auto& vfs = workerd::VirtualFileSystem::current(js);
151 KJ_SWITCH_ONEOF(pathOrFd) {
152 KJ_CASE_ONEOF(path, FilePath) {
153 NormalizedFilePath normalizedPath(kj::mv(path));
154 KJ_IF_SOME(node,
155 vfs.resolve(
156 js, normalizedPath, {.followLinks = options.followSymlinks.orDefault(true)})) {
157 KJ_SWITCH_ONEOF(node) {
158 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
159 return Stat(file->stat(js));
160 }
161 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
162 return Stat(dir->stat(js));
163 }
164 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
165 // If a symbolic link is returned here then the options.followSymLinks
166 // must have been set to false.
167 return Stat(link->stat(js));
168 }
169 KJ_CASE_ONEOF(err, workerd::FsError) {
170 // If we got here, then the path was not found.
171 throwFsError(js, err, "stat"_kj);
172 }
173 }
174 KJ_UNREACHABLE;
175 }
176 }
177 KJ_CASE_ONEOF(fd, int) {
178 KJ_IF_SOME(opened, vfs.tryGetFd(js, fd)) {
179 KJ_SWITCH_ONEOF(opened->node) {
180 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
181 return Stat(file->stat(js));
182 }
183 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
184 return Stat(dir->stat(js));
185 }
186 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
187 return Stat(link->stat(js));
188 }
189 }
190 KJ_UNREACHABLE;
191 } else {
192 node::THROW_ERR_UV_EBADF(js, "fstat"_kj);
193 }
194 }
195 }
196 return kj::none;
197}
198 
199void FileSystemModule::setLastModified(
200 jsg::Lock& js, kj::OneOf<int, FilePath> pathOrFd, kj::Date lastModified, StatOptions options) {
201 auto& vfs = workerd::VirtualFileSystem::current(js);
202 KJ_SWITCH_ONEOF(pathOrFd) {
203 KJ_CASE_ONEOF(path, FilePath) {
204 NormalizedFilePath normalizedPath(kj::mv(path));
205 KJ_IF_SOME(node,
206 vfs.resolve(
207 js, normalizedPath, {.followLinks = options.followSymlinks.orDefault(true)})) {
208 KJ_SWITCH_ONEOF(node) {
209 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
210 KJ_IF_SOME(err, file->setLastModified(js, lastModified)) {
211 // If we got here, then the file is read-only.
212 throwFsError(js, err, "futimes"_kj);
213 }
214 return;
215 }
216 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
217 // Do nothing
218 return;
219 }
220 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
221 // If we got here, then followSymLinks was set to false. We cannot
222 // change the last modified time of a symbolic link in our vfs so
223 // we do nothing.
224 return;
225 }
226 KJ_CASE_ONEOF(err, workerd::FsError) {
227 // If we got here, then the path was not found.
228 throwFsError(js, err, "futimes"_kj);
229 }
230 }
231 } else {
232 node::THROW_ERR_UV_ENOENT(
233 js, "utimes"_kj, nullptr, kj::str(normalizedPath.url.getPathname()));
234 }
235 }
236 KJ_CASE_ONEOF(fd, int) {
237 KJ_IF_SOME(opened, vfs.tryGetFd(js, fd)) {
238 KJ_SWITCH_ONEOF(opened->node) {
239 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
240 KJ_IF_SOME(err, file->setLastModified(js, lastModified)) {
241 throwFsError(js, err, "futimes"_kj);
242 }
243 return;
244 }
245 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
246 // Do nothing
247 return;
248 }
249 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
250 // Do nothing
251 return;
252 }
253 }
254 KJ_UNREACHABLE;
255 } else {
256 node::THROW_ERR_UV_EBADF(js, "futimes"_kj);
257 }
258 }
259 }
260 KJ_UNREACHABLE;
261}
262 
263void FileSystemModule::truncate(jsg::Lock& js, kj::OneOf<int, FilePath> pathOrFd, uint32_t size) {
264 auto& vfs = workerd::VirtualFileSystem::current(js);
265 KJ_SWITCH_ONEOF(pathOrFd) {
266 KJ_CASE_ONEOF(path, FilePath) {
267 NormalizedFilePath normalizedPath(kj::mv(path));
268 KJ_IF_SOME(node, vfs.resolve(js, normalizedPath)) {
269 KJ_SWITCH_ONEOF(node) {
270 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
271 KJ_IF_SOME(err, file->resize(js, size)) {
272 throwFsError(js, err, "ftruncate"_kj);
273 }
274 return;
275 }
276 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
277 node::THROW_ERR_UV_EISDIR(js, "ftruncate"_kj);
278 }
279 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
280 // If we got here, then followSymLinks was set to false. We cannot
281 // truncate a symbolic link.
282 node::THROW_ERR_UV_EINVAL(js, "ftruncate"_kj);
283 }
284 KJ_CASE_ONEOF(err, workerd::FsError) {
285 // If we got here, then the path was not found.
286 throwFsError(js, err, "ftruncate"_kj);
287 }
288 }
289 } else {
290 node::THROW_ERR_UV_ENOENT(
291 js, "ftruncate"_kj, nullptr, kj::str(normalizedPath.url.getPathname()));
292 }
293 }
294 KJ_CASE_ONEOF(fd, int) {
295 KJ_IF_SOME(opened, vfs.tryGetFd(js, fd)) {
296 KJ_SWITCH_ONEOF(opened->node) {
297 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
298 KJ_IF_SOME(err, file->resize(js, size)) {
299 throwFsError(js, err, "ftruncate"_kj);
300 }
301 return;
302 }
303 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
304 node::THROW_ERR_UV_EISDIR(js, "ftruncate"_kj);
305 }
306 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
307 node::THROW_ERR_UV_EINVAL(js, "ftruncate"_kj);
308 }
309 }
310 KJ_UNREACHABLE;
311 } else {
312 node::THROW_ERR_UV_EBADF(js, "ftruncate"_kj);
313 }
314 }
315 }
316 KJ_UNREACHABLE;
317}
318 
319kj::String FileSystemModule::readLink(jsg::Lock& js, FilePath path, ReadLinkOptions options) {
320 auto& vfs = workerd::VirtualFileSystem::current(js);
321 NormalizedFilePath normalizedPath(kj::mv(path));
322 KJ_IF_SOME(node, vfs.resolve(js, normalizedPath, {.followLinks = false})) {
323 KJ_SWITCH_ONEOF(node) {
324 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
325 if (options.failIfNotSymlink) {
326 node::THROW_ERR_UV_EINVAL(js, "readlink"_kj);
327 }
328 kj::Path path = normalizedPath;
329 return path.toString(true);
330 }
331 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
332 if (options.failIfNotSymlink) {
333 node::THROW_ERR_UV_EINVAL(js, "readlink"_kj);
334 }
335 kj::Path path = normalizedPath;
336 return path.toString(true);
337 }
338 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
339 return link->getTargetPath().toString(true);
340 }
341 KJ_CASE_ONEOF(err, workerd::FsError) {
342 // If we got here, then the path was not found.
343 throwFsError(js, err, "readlink"_kj, kj::str(normalizedPath.url.getPathname()));
344 }
345 }
346 KJ_UNREACHABLE;
347 } else {
348 node::THROW_ERR_UV_ENOENT(
349 js, "readlink"_kj, nullptr, kj::str(normalizedPath.url.getPathname()));
350 }
351}
352 
353void FileSystemModule::link(jsg::Lock& js, FilePath from, FilePath to, LinkOptions options) {
354 // The from argument is where we are creating the link, while the to is the target.
355 auto& vfs = workerd::VirtualFileSystem::current(js);
356 NormalizedFilePath normalizedFrom(kj::mv(from));
357 NormalizedFilePath normalizedTo(kj::mv(to));
358 
359 // First, let's make sure the destination (from) does not already exist.
360 const jsg::Url& fromUrl = normalizedFrom;
361 const jsg::Url& toUrl = normalizedTo;
362 
363 KJ_IF_SOME(maybeNode, vfs.resolve(js, fromUrl)) {
364 KJ_IF_SOME(err, maybeNode.tryGet<workerd::FsError>()) {
365 throwFsError(js, err, "link"_kj);
366 }
367 // If we got here, then the destination already exists.
368 node::THROW_ERR_UV_EEXIST(js, "link"_kj, "File already exists"_kj);
369 }
370 
371 // Now, let's split the fromUrl into a base directory URL and a file name so
372 // that we can make sure the destination directory exists.
373 jsg::Url::Relative fromRelative = fromUrl.getRelative();
374 
375 if (fromRelative.name.size() == 0) {
376 node::THROW_ERR_UV_EINVAL(js, "link"_kj, "Invalid filename"_kj);
377 }
378 
379 KJ_IF_SOME(parent, vfs.resolve(js, fromRelative.base)) {
380 KJ_IF_SOME(dir, parent.tryGet<kj::Rc<workerd::Directory>>()) {
381 // Dir is where the new link will go. fromRelative.name is the name of
382 // the new link in this directory.
383 
384 // If we are creating a symbolic link, we do not need to check if the target exists.
385 if (options.symbolic) {
386 KJ_IF_SOME(err, dir->add(js, fromRelative.name, vfs.newSymbolicLink(js, toUrl))) {
387 throwFsError(js, err, "link"_kj);
388 }
389 return;
390 }
391 
392 // If we are creating a hard link, however, the target must exist.
393 KJ_IF_SOME(target, vfs.resolve(js, toUrl, {.followLinks = false})) {
394 KJ_SWITCH_ONEOF(target) {
395 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
396 KJ_IF_SOME(err, dir->add(js, fromRelative.name, file.addRef())) {
397 throwFsError(js, err, "link"_kj);
398 }
399 }
400 KJ_CASE_ONEOF(tdir, kj::Rc<workerd::Directory>) {
401 // It is not permitted to hardlink to a directory.
402 node::THROW_ERR_UV_EPERM(js, "link"_kj, "Cannot hardlink to a directory"_kj);
403 }
404 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
405 KJ_IF_SOME(err, dir->add(js, fromRelative.name, link.addRef())) {
406 throwFsError(js, err, "link"_kj);
407 }
408 }
409 KJ_CASE_ONEOF(err, workerd::FsError) {
410 // If we got here, then the target path was not found.
411 throwFsError(js, err, "link"_kj);
412 }
413 }
414 } else {
415 node::THROW_ERR_UV_ENOENT(js, "link"_kj, "File not found"_kj, kj::str(toUrl.getPathname()));
416 }
417 } else {
418 node::THROW_ERR_UV_EINVAL(js, "link"_kj, "Not a directory"_kj);
419 }
420 } else {
421 node::THROW_ERR_UV_ENOENT(
422 js, "link"_kj, "Directory does not exist"_kj, kj::str(toUrl.getPathname()));
423 }
424}
425 
426void FileSystemModule::unlink(jsg::Lock& js, FilePath path) {
427 auto& vfs = workerd::VirtualFileSystem::current(js);
428 NormalizedFilePath normalizedPath(kj::mv(path));
429 const jsg::Url& url = normalizedPath;
430 auto relative = url.getRelative();
431 
432 KJ_IF_SOME(parent, vfs.resolve(js, relative.base)) {
433 KJ_IF_SOME(dir, parent.tryGet<kj::Rc<workerd::Directory>>()) {
434 kj::Path fpath(relative.name);
435 KJ_IF_SOME(stat, dir->stat(js, fpath)) {
436 KJ_SWITCH_ONEOF(stat) {
437 KJ_CASE_ONEOF(stat, workerd::Stat) {
438 if (stat.type == FsType::DIRECTORY) {
439 node::THROW_ERR_UV_EISDIR(js, "unlink"_kj, "Cannot unlink a directory"_kj);
440 }
441 }
442 KJ_CASE_ONEOF(err, workerd::FsError) {
443 throwFsError(js, err, "unlink"_kj);
444 }
445 }
446 } else {
447 node::THROW_ERR_UV_ENOENT(js, "unlink"_kj, "File not found"_kj, relative.name);
448 }
449 
450 KJ_SWITCH_ONEOF(dir->remove(js, fpath)) {
451 KJ_CASE_ONEOF(res, bool) {
452 // Ignore the return.
453 }
454 KJ_CASE_ONEOF(err, workerd::FsError) {
455 throwFsError(js, err, "unlink"_kj);
456 }
457 }
458 } else {
459 node::THROW_ERR_UV_ENOTDIR(js, "unlink"_kj, "Parent path is not a directory"_kj);
460 }
461 } else {
462 node::THROW_ERR_UV_ENOENT(js, "unlink"_kj, "File not found"_kj, relative.name);
463 }
464}
465 
466int FileSystemModule::open(jsg::Lock& js, FilePath path, OpenOptions options) {
467 auto& vfs = workerd::VirtualFileSystem::current(js);
468 NormalizedFilePath normalizedPath(kj::mv(path));
469 KJ_SWITCH_ONEOF(vfs.openFd(js, normalizedPath,
470 workerd::VirtualFileSystem::OpenOptions{
471 .read = options.read,
472 .write = options.write,
473 .append = options.append,
474 .exclusive = options.exclusive,
475 .followLinks = options.followSymlinks,
476 })) {
477 KJ_CASE_ONEOF(opened, kj::Rc<workerd::VirtualFileSystem::OpenedFile>) {
478 return opened->fd;
479 }
480 KJ_CASE_ONEOF(err, workerd::FsError) {
481 throwFsError(js, err, "open"_kj);
482 }
483 }
484 KJ_UNREACHABLE;
485}
486 
487void FileSystemModule::close(jsg::Lock& js, int fd) {
488 auto& vfs = workerd::VirtualFileSystem::current(js);
489 vfs.closeFd(js, fd);
490}
491 
492uint32_t FileSystemModule::write(
493 jsg::Lock& js, int fd, kj::Array<jsg::BufferSource> data, WriteOptions options) {
494 auto& vfs = workerd::VirtualFileSystem::current(js);
495 
496 KJ_IF_SOME(opened, vfs.tryGetFd(js, fd)) {
497 static const auto getPosition = [](jsg::Lock& js, auto opened, auto file,
498 const WriteOptions& options) -> uint32_t {
499 if (opened->append) {
500 // If the file descriptor is opened in append mode, we ignore the position
501 // option and always append to the end of the file.
502 auto stat = file->stat(js);
503 return stat.size;
504 }
505 auto pos = options.position.orDefault(opened->position);
506 if (pos > kMax) {
507 node::THROW_ERR_UV_EINVAL(js, "write"_kj, "position out of range"_kj);
508 }
509 return static_cast<uint32_t>(pos);
510 };
511 KJ_SWITCH_ONEOF(opened->node) {
512 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
513 auto pos = getPosition(js, opened.addRef(), file.addRef(), options);
514 uint32_t total = 0;
515 for (auto& buffer: data) {
516 KJ_SWITCH_ONEOF(file->write(js, pos, buffer)) {
517 KJ_CASE_ONEOF(written, uint32_t) {
518 pos += written;
519 total += written;
520 }
521 KJ_CASE_ONEOF(err, workerd::FsError) {
522 throwFsError(js, err, "write"_kj);
523 }
524 }
525 }
526 // We only update the position if the options.position is not set and
527 // the file descriptor is not opened in append mode.
528 if (options.position == kj::none && !opened->append) {
529 opened->position += total;
530 }
531 return total;
532 }
533 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
534 node::THROW_ERR_UV_EISDIR(js, "write"_kj);
535 }
536 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
537 // If we get here, then followSymLinks was set to false when open was called.
538 // We can't write to a symbolic link.
539 node::THROW_ERR_UV_EINVAL(js, "write"_kj);
540 }
541 }
542 KJ_UNREACHABLE;
543 } else {
544 node::THROW_ERR_UV_EBADF(js, "write"_kj);
545 }
546}
547 
548uint32_t FileSystemModule::read(
549 jsg::Lock& js, int fd, kj::Array<jsg::BufferSource> data, WriteOptions options) {
550 auto& vfs = workerd::VirtualFileSystem::current(js);
551 KJ_IF_SOME(opened, vfs.tryGetFd(js, fd)) {
552 if (!opened->read) {
553 node::THROW_ERR_UV_EBADF(js, "read"_kj);
554 }
555 
556 KJ_SWITCH_ONEOF(opened->node) {
557 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
558 auto pos = options.position.orDefault(opened->position);
559 if (pos > kMax) {
560 node::THROW_ERR_UV_EINVAL(js, "read"_kj, "position out of range"_kj);
561 }
562 uint32_t total = 0;
563 for (auto& buffer: data) {
564 auto read = file->read(js, pos, buffer);
565 // if read is less than the size of the buffer, we are at EOF.
566 pos += read;
567 total += read;
568 if (read < buffer.size()) break;
569 }
570 // We only update the position if the options.position is not set.
571 if (options.position == kj::none) {
572 opened->position += total;
573 }
574 return total;
575 }
576 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
577 node::THROW_ERR_UV_EISDIR(js, "read"_kj);
578 }
579 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
580 // If we get here, then followSymLinks was set to false when open was called.
581 // We can't read from a symbolic link.
582 node::THROW_ERR_UV_EINVAL(js, "read"_kj);
583 }
584 }
585 KJ_UNREACHABLE;
586 } else {
587 node::THROW_ERR_UV_EBADF(js, "read"_kj);
588 }
589}
590 
591jsg::BufferSource FileSystemModule::readAll(jsg::Lock& js, kj::OneOf<int, FilePath> pathOrFd) {
592 auto& vfs = workerd::VirtualFileSystem::current(js);
593 KJ_SWITCH_ONEOF(pathOrFd) {
594 KJ_CASE_ONEOF(path, FilePath) {
595 NormalizedFilePath normalized(kj::mv(path));
596 KJ_IF_SOME(node, vfs.resolve(js, normalized)) {
597 KJ_SWITCH_ONEOF(node) {
598 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
599 KJ_SWITCH_ONEOF(file->readAllBytes(js)) {
600 KJ_CASE_ONEOF(data, jsg::BufferSource) {
601 return kj::mv(data);
602 }
603 KJ_CASE_ONEOF(err, workerd::FsError) {
604 throwFsError(js, err, "readAll"_kj);
605 }
606 }
607 KJ_UNREACHABLE;
608 }
609 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
610 node::THROW_ERR_UV_EISDIR(js, "readAll"_kj);
611 }
612 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
613 // We shouldn't be able to get here since we are following symlinks.
614 KJ_UNREACHABLE;
615 }
616 KJ_CASE_ONEOF(err, workerd::FsError) {
617 throwFsError(js, err, "readAll"_kj);
618 }
619 }
620 } else {
621 node::THROW_ERR_UV_ENOENT(js, "readAll"_kj, nullptr, kj::str(normalized.url.getPathname()));
622 }
623 }
624 KJ_CASE_ONEOF(fd, int) {
625 KJ_IF_SOME(opened, vfs.tryGetFd(js, fd)) {
626 if (!opened->read) {
627 node::THROW_ERR_UV_EBADF(js, "fread"_kj);
628 }
629 
630 KJ_IF_SOME(file, opened->node.tryGet<kj::Rc<workerd::File>>()) {
631 // Move the opened.position to the end of the file.
632 KJ_DEFER({
633 auto stat = file->stat(js);
634 opened->position = stat.size;
635 });
636 
637 KJ_SWITCH_ONEOF(file->readAllBytes(js)) {
638 KJ_CASE_ONEOF(data, jsg::BufferSource) {
639 return kj::mv(data);
640 }
641 KJ_CASE_ONEOF(err, workerd::FsError) {
642 throwFsError(js, err, "freadAll"_kj);
643 }
644 }
645 KJ_UNREACHABLE;
646 } else {
647 node::THROW_ERR_UV_EBADF(js, "fread"_kj);
648 }
649 } else {
650 node::THROW_ERR_UV_EBADF(js, "fread"_kj);
651 }
652 }
653 }
654 KJ_UNREACHABLE;
655}
656 
657uint32_t FileSystemModule::writeAll(jsg::Lock& js,
658 kj::OneOf<int, FilePath> pathOrFd,
659 jsg::BufferSource data,
660 WriteAllOptions options) {
661 auto& vfs = workerd::VirtualFileSystem::current(js);
662 
663 if (data.size() > kMax) {
664 node::THROW_ERR_UV_EFBIG(js, "writeAll"_kj);
665 }
666 
667 KJ_SWITCH_ONEOF(pathOrFd) {
668 KJ_CASE_ONEOF(path, FilePath) {
669 NormalizedFilePath normalized(kj::mv(path));
670 KJ_IF_SOME(node, vfs.resolve(js, normalized)) {
671 // If the exclusive option is set, the file must not already exist.
672 if (options.exclusive) {
673 node::THROW_ERR_UV_EEXIST(js, "writeAll"_kj, "file already exists"_kj);
674 }
675 // The file exists, we can write to it.
676 KJ_SWITCH_ONEOF(node) {
677 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
678 // First let's check that the file is writable.
679 auto stat = file->stat(js);
680 if (!stat.writable) {
681 node::THROW_ERR_UV_EPERM(js, "writeAll"_kj);
682 }
683 
684 // If the append option is set, we will write to the end of the file
685 // instead of overwriting it.
686 if (options.append) {
687 KJ_SWITCH_ONEOF(file->write(js, stat.size, data)) {
688 KJ_CASE_ONEOF(written, uint32_t) {
689 return written;
690 }
691 KJ_CASE_ONEOF(err, workerd::FsError) {
692 throwFsError(js, err, "writeAll"_kj);
693 }
694 }
695 KJ_UNREACHABLE;
696 }
697 
698 // Otherwise, we overwrite the entire file.
699 KJ_SWITCH_ONEOF(file->writeAll(js, data)) {
700 KJ_CASE_ONEOF(written, uint32_t) {
701 return written;
702 }
703 KJ_CASE_ONEOF(err, workerd::FsError) {
704 throwFsError(js, err, "writeAll"_kj);
705 }
706 }
707 KJ_UNREACHABLE;
708 }
709 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
710 node::THROW_ERR_UV_EISDIR(js, "writeAll"_kj);
711 }
712 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
713 // If we get here, then followSymLinks was set to false when open was called.
714 // We can't write to a symbolic link.
715 node::THROW_ERR_UV_EINVAL(js, "writeAll"_kj);
716 }
717 KJ_CASE_ONEOF(err, workerd::FsError) {
718 throwFsError(js, err, "writeAll"_kj);
719 }
720 }
721 KJ_UNREACHABLE;
722 }
723 // The file does not exist. We first need to create it, then write to it.
724 // Let's make sure the parent directory exists.
725 const jsg::Url& url = normalized;
726 jsg::Url::Relative relative = url.getRelative();
727 
728 KJ_IF_SOME(parent, vfs.resolve(js, relative.base)) {
729 // Let's make sure the parent is a directory.
730 KJ_SWITCH_ONEOF(parent) {
731 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
732 node::THROW_ERR_UV_ENOTDIR(js, "writeAll"_kj);
733 }
734 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
735 auto stat = dir->stat(js);
736 if (!stat.writable) {
737 node::THROW_ERR_UV_EPERM(js, "writeAll"_kj);
738 }
739 auto file = workerd::File::newWritable(js, static_cast<uint32_t>(data.size()));
740 KJ_SWITCH_ONEOF(file->writeAll(js, data)) {
741 KJ_CASE_ONEOF(written, uint32_t) {
742 KJ_IF_SOME(err, dir->add(js, relative.name, kj::mv(file))) {
743 throwFsError(js, err, "writeAll"_kj);
744 }
745 return written;
746 }
747 KJ_CASE_ONEOF(err, workerd::FsError) {
748 throwFsError(js, err, "writeAll"_kj);
749 }
750 }
751 }
752 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
753 // If we get here, then followSymLinks was set to false when open was called.
754 // We can't write to a symbolic link.
755 node::THROW_ERR_UV_EINVAL(js, "writeAll"_kj);
756 }
757 KJ_CASE_ONEOF(err, workerd::FsError) {
758 // If we got here, then the parent path was not found.
759 throwFsError(js, err, "writeAll"_kj);
760 }
761 }
762 KJ_UNREACHABLE;
763 } else {
764 node::THROW_ERR_UV_ENOENT(
765 js, "writeAll"_kj, nullptr, kj::str(normalized.url.getPathname()));
766 }
767 }
768 KJ_CASE_ONEOF(fd, int) {
769 KJ_IF_SOME(opened, vfs.tryGetFd(js, fd)) {
770 // Otherwise, we'll overwrite the file...
771 if (!opened->write) {
772 node::THROW_ERR_UV_EBADF(js, "fwrite"_kj);
773 }
774 
775 KJ_IF_SOME(file, opened->node.tryGet<kj::Rc<workerd::File>>()) {
776 auto stat = file->stat(js);
777 
778 if (!stat.writable) {
779 node::THROW_ERR_UV_EPERM(js, "fwrite"_kj);
780 }
781 
782 KJ_DEFER({
783 // In either case, we need to update the position of the file descriptor.
784 stat = file->stat(js);
785 opened->position = stat.size;
786 });
787 
788 // If the file descriptor was opened in append mode, or if the append option
789 // is set, then we'll use write instead to append to the end of the file.
790 if (opened->append || options.append) {
791 return write(js, fd, kj::arr(kj::mv(data)),
792 {
793 .position = stat.size,
794 });
795 }
796 
797 // Otherwise, we overwrite the entire file.
798 KJ_SWITCH_ONEOF(file->writeAll(js, data)) {
799 KJ_CASE_ONEOF(written, uint32_t) {
800 return written;
801 }
802 KJ_CASE_ONEOF(err, workerd::FsError) {
803 throwFsError(js, err, "fwriteAll"_kj);
804 }
805 }
806 KJ_UNREACHABLE;
807 } else {
808 node::THROW_ERR_UV_EBADF(js, "fwrite"_kj);
809 }
810 } else {
811 node::THROW_ERR_UV_EBADF(js, "fwrite"_kj);
812 }
813 }
814 }
815 
816 KJ_UNREACHABLE;
817}
818 
819void FileSystemModule::renameOrCopy(
820 jsg::Lock& js, FilePath src, FilePath dest, RenameOrCopyOptions options) {
821 // The source must exist, the destination must not.
822 auto& vfs = workerd::VirtualFileSystem::current(js);
823 NormalizedFilePath normalizedSrc(kj::mv(src));
824 NormalizedFilePath normalizedDest(kj::mv(dest));
825 
826 const jsg::Url& destUrl = normalizedDest;
827 const jsg::Url& srcUrl = normalizedSrc;
828 
829 auto opName = options.copy ? "copy"_kj : "rename"_kj;
830 
831 KJ_IF_SOME(maybeDestNode, vfs.resolve(js, destUrl)) {
832 KJ_IF_SOME(err, maybeDestNode.tryGet<workerd::FsError>()) {
833 throwFsError(js, err, "rename"_kj);
834 }
835 node::THROW_ERR_UV_EEXIST(js, opName);
836 }
837 
838 jsg::Url::Relative relative = destUrl.getRelative();
839 // The destination parent must exist.
840 KJ_IF_SOME(parent, vfs.resolve(js, relative.base)) {
841 KJ_SWITCH_ONEOF(parent) {
842 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
843 node::THROW_ERR_UV_ENOTDIR(js, opName);
844 }
845 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
846 kj::Maybe<kj::Rc<Directory>> srcParent;
847 if (!options.copy) {
848 // If we are not copying, let's make sure that the source directory is writable
849 // before we actually try moving it.
850 auto relative = srcUrl.getRelative();
851 KJ_IF_SOME(parent, vfs.resolve(js, relative.base)) {
852 KJ_SWITCH_ONEOF(parent) {
853 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
854 node::THROW_ERR_UV_ENOTDIR(js, opName);
855 }
856 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
857 // We can only rename a file or directory if the parent is writable.
858 // If the parent is not writable, we throw an error.
859 auto stat = dir->stat(js);
860 if (!stat.writable) {
861 node::THROW_ERR_UV_EPERM(js, opName);
862 }
863 srcParent = dir.addRef();
864 }
865 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
866 node::THROW_ERR_UV_ENOTDIR(js, opName);
867 }
868 KJ_CASE_ONEOF(err, workerd::FsError) {
869 // If we got here, then the parent path was not found.
870 throwFsError(js, err, opName);
871 }
872 }
873 } else {
874 node::THROW_ERR_UV_ENOENT(js, opName, nullptr, relative.name);
875 }
876 }
877 
878 KJ_IF_SOME(srcNode, vfs.resolve(js, normalizedSrc)) {
879 // The next part is easy. We either clone or add ref the original node and add it to the
880 // destination directory.
881 KJ_SWITCH_ONEOF(srcNode) {
882 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
883 kj::OneOf<workerd::FsError, kj::Rc<workerd::File>> errOrFile =
884 options.copy ? file->clone(js) : file.addRef();
885 KJ_SWITCH_ONEOF(errOrFile) {
886 KJ_CASE_ONEOF(err, workerd::FsError) {
887 throwFsError(js, err, "cp"_kj);
888 }
889 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
890 KJ_IF_SOME(err, dir->add(js, relative.name, kj::mv(file))) {
891 throwFsError(js, err, opName);
892 }
893 }
894 }
895 }
896 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
897 if (options.copy) {
898 node::THROW_ERR_UV_EISDIR(js, opName);
899 }
900 KJ_IF_SOME(err, dir->add(js, relative.name, dir.addRef())) {
901 throwFsError(js, err, opName);
902 }
903 }
904 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
905 KJ_IF_SOME(err, dir->add(js, relative.name, link.addRef())) {
906 throwFsError(js, err, opName);
907 }
908 }
909 KJ_CASE_ONEOF(err, workerd::FsError) {
910 throwFsError(js, err, opName);
911 }
912 }
913 
914 KJ_IF_SOME(dir, srcParent) {
915 auto relative = srcUrl.getRelative();
916 KJ_SWITCH_ONEOF(dir->remove(js, kj::Path({relative.name}), {.recursive = true})) {
917 KJ_CASE_ONEOF(_, bool) {
918 // ignore the specific return value.
919 return;
920 }
921 KJ_CASE_ONEOF(err, workerd::FsError) {
922 throwFsError(js, err, "rename"_kj);
923 }
924 }
925 KJ_UNREACHABLE;
926 }
927 } else {
928 node::THROW_ERR_UV_ENOENT(js, opName, nullptr, kj::str(normalizedSrc.url.getPathname()));
929 }
930 }
931 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
932 node::THROW_ERR_UV_ENOTDIR(js, opName);
933 }
934 KJ_CASE_ONEOF(err, workerd::FsError) {
935 // If we got here, then the parent path was not found.
936 throwFsError(js, err, opName);
937 }
938 }
939 } else {
940 node::THROW_ERR_UV_ENOENT(js, opName, nullptr, relative.name);
941 }
942}
943 
944jsg::Optional<kj::String> FileSystemModule::mkdir(
945 jsg::Lock& js, FilePath path, MkdirOptions options) {
946 auto& vfs = workerd::VirtualFileSystem::current(js);
947 NormalizedFilePath normalizedPath(kj::mv(path));
948 const jsg::Url& url = normalizedPath;
949 
950 // The path must not already exist. However, if the path is a directory, we
951 // will just return rather than throwing an error.
952 KJ_IF_SOME(node, vfs.resolve(js, url, {.followLinks = false})) {
953 KJ_SWITCH_ONEOF(node) {
954 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
955 node::THROW_ERR_UV_EEXIST(js, "mkdir"_kj);
956 }
957 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
958 // The directory already exists. We will just return.
959 return kj::none;
960 }
961 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
962 node::THROW_ERR_UV_EEXIST(js, "mkdir"_kj);
963 }
964 KJ_CASE_ONEOF(err, workerd::FsError) {
965 throwFsError(js, err, "mkdir"_kj);
966 }
967 }
968 KJ_UNREACHABLE;
969 };
970 
971 if (options.recursive) {
972 KJ_ASSERT(!options.tmp);
973 // If the recursive option is set, we will create all the directories in the
974 // path that do not exist, returning the path to the first one that was created.
975 const kj::Path kjPath = normalizedPath;
976 const auto parentPath = kjPath.parent();
977 const auto name = kjPath.basename();
978 kj::Maybe<kj::String> createdPath;
979 
980 // We'll start from the root and work our way down.
981 auto current = vfs.getRoot(js);
982 kj::Path currentPath{};
983 for (const auto& part: parentPath) {
984 currentPath = currentPath.append(part);
985 bool moveToNext = false;
986 // Try opening the next part of the path. Note that we are not using the
987 // createAs option here because we don't necessarily want to implicitly
988 // create the directory if it doesn't exist. We want to create it explicitly
989 // so that we can return the path to the first directory that was created
990 // and tryOpen does not us if the directory already existed or was created.
991 KJ_IF_SOME(node, current->tryOpen(js, kj::Path({part}))) {
992 // Let's make sure the node is a directory.
993 KJ_SWITCH_ONEOF(node) {
994 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
995 node::THROW_ERR_UV_ENOTDIR(js, "mkdir"_kj);
996 }
997 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
998 node::THROW_ERR_UV_ENOTDIR(js, "mkdir"_kj);
999 }
1000 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1001 // The node is a directory, we can continue.
1002 current = kj::mv(dir);
1003 moveToNext = true;
1004 }
1005 KJ_CASE_ONEOF(err, workerd::FsError) {
1006 throwFsError(js, err, "mkdir"_kj);
1007 }
1008 }
1009 }
1010 if (moveToNext) continue;
1011 
1012 // The node does not exist, let's create it so long as the current
1013 // directory is writable.
1014 auto stat = current->stat(js);
1015 if (!stat.writable) {
1016 node::THROW_ERR_UV_EPERM(js, "mkdir"_kj);
1017 }
1018 auto dir = workerd::Directory::newWritable(js);
1019 KJ_IF_SOME(err, current->add(js, part, dir.addRef())) {
1020 throwFsError(js, err, "mkdir"_kj);
1021 }
1022 current = kj::mv(dir);
1023 if (createdPath == kj::none) {
1024 createdPath = currentPath.toString(true);
1025 }
1026 }
1027 
1028 // Now that we have the parent directory, let's try creating the new directory.
1029 auto newDir = workerd::Directory::newWritable(js);
1030 KJ_IF_SOME(err, current->add(js, name.toString(false), kj::mv(newDir))) {
1031 throwFsError(js, err, "mkdir"_kj);
1032 }
1033 
1034 return kj::mv(createdPath);
1035 }
1036 
1037 KJ_DASSERT(!options.recursive);
1038 // If the recursive option is not set, we will create the directory only if
1039 // the parent directory exists. If the parent directory does not exist, we
1040 // will return an error.
1041 jsg::Url::Relative relative = url.getRelative();
1042 KJ_IF_SOME(parent, vfs.resolve(js, relative.base)) {
1043 KJ_SWITCH_ONEOF(parent) {
1044 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1045 node::THROW_ERR_UV_ENOTDIR(js, "mkdir"_kj);
1046 }
1047 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1048 auto stat = dir->stat(js);
1049 if (!stat.writable) {
1050 node::THROW_ERR_UV_EPERM(js, "mkdir"_kj);
1051 }
1052 auto newDir = workerd::Directory::newWritable(js);
1053 if (options.tmp) {
1054 if (tmpFileCounter >= kMax) {
1055 node::THROW_ERR_UV_EPERM(js, "mkdir"_kj, "Too many temporary directories created"_kj);
1056 }
1057 auto name = kj::str(relative.name, tmpFileCounter++);
1058 KJ_IF_SOME(err, dir->add(js, name, kj::mv(newDir))) {
1059 throwFsError(js, err, "mkdir"_kj);
1060 }
1061 KJ_IF_SOME(newUrl, relative.base.resolve(name)) {
1062 // If we are creating a temporary directory, we return the URL of the
1063 // new directory.
1064 return kj::str(newUrl.getPathname());
1065 } else {
1066 node::THROW_ERR_UV_EINVAL(js, "mkdir"_kj, "Invalid name for temporary directory"_kj);
1067 }
1068 }
1069 
1070 KJ_IF_SOME(err, dir->add(js, relative.name, kj::mv(newDir))) {
1071 throwFsError(js, err, "mkdir"_kj);
1072 }
1073 
1074 return kj::none;
1075 }
1076 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1077 node::THROW_ERR_UV_ENOTDIR(js, "mkdir"_kj);
1078 }
1079 KJ_CASE_ONEOF(err, workerd::FsError) {
1080 throwFsError(js, err, "mkdir"_kj, relative.name);
1081 }
1082 }
1083 KJ_UNREACHABLE;
1084 } else {
1085 node::THROW_ERR_UV_ENOENT(js, "mkdir"_kj, nullptr, relative.name);
1086 }
1087}
1088 
1089void FileSystemModule::rm(jsg::Lock& js, FilePath path, RmOptions options) {
1090 // TODO(node-fs): Implement the force option.
1091 auto& vfs = workerd::VirtualFileSystem::current(js);
1092 NormalizedFilePath normalizedPath(kj::mv(path));
1093 const jsg::Url& url = normalizedPath;
1094 jsg::Url::Relative relative = url.getRelative();
1095 
1096 KJ_IF_SOME(parent, vfs.resolve(js, relative.base)) {
1097 KJ_IF_SOME(dir, parent.tryGet<kj::Rc<workerd::Directory>>()) {
1098 auto stat = dir->stat(js);
1099 if (!stat.writable) {
1100 node::THROW_ERR_UV_EPERM(js, "rm"_kj);
1101 }
1102 
1103 kj::Path name(relative.name);
1104 
1105 if (options.dironly) {
1106 // If the dironly option is set, we will only remove the entry if it is a directory.
1107 KJ_IF_SOME(stat, dir->stat(js, name)) {
1108 KJ_SWITCH_ONEOF(stat) {
1109 KJ_CASE_ONEOF(stat, workerd::Stat) {
1110 if (stat.type != workerd::FsType::DIRECTORY) {
1111 node::THROW_ERR_UV_ENOTDIR(js, "rm"_kj);
1112 }
1113 }
1114 KJ_CASE_ONEOF(err, workerd::FsError) {
1115 throwFsError(js, err, "rm"_kj);
1116 }
1117 }
1118 } else {
1119 node::THROW_ERR_UV_ENOENT(js, "rm"_kj, nullptr, relative.name);
1120 }
1121 }
1122 
1123 KJ_SWITCH_ONEOF(dir->remove(js, name, {.recursive = options.recursive})) {
1124 KJ_CASE_ONEOF(res, bool) {
1125 // Ignore the return.
1126 }
1127 KJ_CASE_ONEOF(err, workerd::FsError) {
1128 throwFsError(js, err, "rm"_kj, relative.name);
1129 }
1130 }
1131 } else {
1132 node::THROW_ERR_UV_ENOTDIR(js, "rm"_kj, nullptr, relative.name);
1133 }
1134 } else {
1135 node::THROW_ERR_UV_ENOENT(js, "rm"_kj, nullptr, relative.name);
1136 }
1137}
1138 
1139namespace {
1140static constexpr int UV_DIRENT_FILE = 1;
1141static constexpr int UV_DIRENT_DIR = 2;
1142static constexpr int UV_DIRENT_LINK = 3;
1143static constexpr int UV_DIRENT_CHAR = 6;
1144void readdirImpl(jsg::Lock& js,
1145 const workerd::VirtualFileSystem& vfs,
1146 kj::Rc<workerd::Directory> dir,
1147 const kj::Path& path,
1148 const FileSystemModule::ReadDirOptions& options,
1149 kj::Vector<FileSystemModule::DirEntHandle>& entries) {
1150 for (auto& entry: *dir.get()) {
1151 auto name = options.recursive ? path.append(entry.key).toString(false) : kj::str(entry.key);
1152 KJ_SWITCH_ONEOF(entry.value) {
1153 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1154 auto stat = file->stat(js);
1155 entries.add(FileSystemModule::DirEntHandle{
1156 .name = kj::mv(name),
1157 .parentPath = path.toString(true),
1158 .type = stat.device ? UV_DIRENT_CHAR : UV_DIRENT_FILE,
1159 });
1160 }
1161 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1162 entries.add(FileSystemModule::DirEntHandle{
1163 .name = kj::mv(name),
1164 .parentPath = path.toString(true),
1165 .type = UV_DIRENT_DIR,
1166 });
1167 
1168 if (options.recursive) {
1169 readdirImpl(js, vfs, dir.addRef(), path.append(entry.key), options, entries);
1170 }
1171 }
1172 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1173 entries.add(FileSystemModule::DirEntHandle{
1174 .name = kj::mv(name),
1175 .parentPath = path.toString(true),
1176 .type = UV_DIRENT_LINK,
1177 });
1178 
1179 if (options.recursive) {
1180 workerd::SymbolicLinkRecursionGuardScope guard;
1181 KJ_IF_SOME(err, guard.checkSeen(link.get())) {
1182 throwFsError(js, err, "readdir"_kj);
1183 }
1184 KJ_IF_SOME(target, link->resolve(js)) {
1185 KJ_SWITCH_ONEOF(target) {
1186 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1187 // Do nothing
1188 }
1189 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1190 readdirImpl(js, vfs, dir.addRef(), path.append(entry.key), options, entries);
1191 }
1192 KJ_CASE_ONEOF(err, workerd::FsError) {
1193 throwFsError(js, err, "readdir"_kj);
1194 }
1195 }
1196 }
1197 }
1198 }
1199 }
1200 }
1201}
1202} // namespace
1203 
1204kj::Array<FileSystemModule::DirEntHandle> FileSystemModule::readdir(
1205 jsg::Lock& js, FilePath path, ReadDirOptions options) {
1206 auto& vfs = workerd::VirtualFileSystem::current(js);
1207 NormalizedFilePath normalizedPath(kj::mv(path));
1208 
1209 KJ_IF_SOME(node, vfs.resolve(js, normalizedPath, {.followLinks = false})) {
1210 KJ_SWITCH_ONEOF(node) {
1211 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1212 kj::Vector<DirEntHandle> entries;
1213 readdirImpl(js, vfs, kj::mv(dir), normalizedPath, options, entries);
1214 return entries.releaseAsArray();
1215 }
1216 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1217 node::THROW_ERR_UV_ENOTDIR(
1218 js, "readdir"_kj, nullptr, kj::str(normalizedPath.url.getPathname()));
1219 }
1220 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1221 node::THROW_ERR_UV_EINVAL(
1222 js, "readdir"_kj, nullptr, kj::str(normalizedPath.url.getPathname()));
1223 }
1224 KJ_CASE_ONEOF(err, workerd::FsError) {
1225 throwFsError(js, err, "readdir"_kj, kj::str(normalizedPath.url.getPathname()));
1226 }
1227 }
1228 KJ_UNREACHABLE;
1229 } else {
1230 node::THROW_ERR_UV_ENOENT(js, "readdir"_kj, nullptr, kj::str(normalizedPath.url.getPathname()));
1231 }
1232}
1233 
1234namespace {
1235 
1236using MaybeFsNode = kj::Maybe<
1237 kj::OneOf<kj::Rc<workerd::Directory>, kj::Rc<workerd::File>, kj::Rc<workerd::SymbolicLink>>>;
1238 
1239MaybeFsNode getNodeOrError(jsg::Lock& js,
1240 const workerd::VirtualFileSystem& vfs,
1241 const jsg::Url& url,
1242 const FileSystemModule::CpOptions& options) {
1243 KJ_IF_SOME(node, vfs.resolve(js, url, {.followLinks = options.deferenceSymlinks})) {
1244 KJ_SWITCH_ONEOF(node) {
1245 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1246 return MaybeFsNode(kj::mv(file));
1247 }
1248 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1249 return MaybeFsNode(kj::mv(dir));
1250 }
1251 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1252 return MaybeFsNode(kj::mv(link));
1253 }
1254 KJ_CASE_ONEOF(err, workerd::FsError) {
1255 throwFsError(js, err, "cp"_kj);
1256 }
1257 }
1258 }
1259 return kj::none;
1260}
1261 
1262// Copy the src symbolic link to the destination URL location.
1263// We've already checked that the destination either does not exist
1264// or we are want to overwrite it. We need to next determine if the
1265// destination is writable. If it is not, this will throw an error.
1266// If it is, we will either create a new symbolic link at the destination
1267// or overwrite the existing file or link if it exists.
1268void handleCpLink(jsg::Lock& js,
1269 const workerd::VirtualFileSystem& vfs,
1270 kj::Rc<workerd::SymbolicLink> srcLink,
1271 const jsg::Url& destUrl) {
1272 // Here, we are going to essentially create a hard link (new refcount)
1273 // of srcLink and destUrl, if we are allowed to do so.
1274 // We need to check if the destination exists and is writable.
1275 auto relative = destUrl.getRelative();
1276 // relative.base is the parent directory path.
1277 // relative.name is the name of the link we are creating in the parent.
1278 
1279 auto basePath = kj::str(relative.base.getPathname().slice(1));
1280 kj::Path root{};
1281 auto base = root.eval(basePath);
1282 
1283 // We need to grab the parent directory, creating it if it does not exist
1284 // and we are permitted to do so.
1285 KJ_IF_SOME(destDir,
1286 vfs.getRoot(js)->tryOpen(js, base,
1287 {
1288 .createAs = workerd::FsType::DIRECTORY,
1289 .followLinks = true,
1290 })) {
1291 // Awesome, either the destination directory existed already or we
1292 // successfully created it, or an error was reported.
1293 KJ_SWITCH_ONEOF(destDir) {
1294 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1295 // We cannot copy into a file, so we throw an error.
1296 node::THROW_ERR_UV_ENOTDIR(js, "cp"_kj);
1297 }
1298 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1299 kj::Path path({relative.name});
1300 // This is the case we're looking for!
1301 // First, let's check to see if the target name already exists.
1302 // If it does, we'll remove it.
1303 KJ_SWITCH_ONEOF(dir->remove(js, path, {.recursive = false})) {
1304 KJ_CASE_ONEOF(err, workerd::FsError) {
1305 throwFsError(js, err, "cp"_kj);
1306 }
1307 KJ_CASE_ONEOF(b, bool) {
1308 // Ignore the return value, we don't actually care if the thing existed or not.
1309 }
1310 }
1311 // Now, we can add the symbolic link to the directory.
1312 KJ_IF_SOME(err, dir->add(js, relative.name, kj::mv(srcLink))) {
1313 // If we got here, an error was reported
1314 throwFsError(js, err, "cp"_kj);
1315 }
1316 // If we got here, success!
1317 return;
1318 }
1319 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1320 // This shouldn't be possible since we told tryOpen to follow links.
1321 // But, let's just throw an error.
1322 node::THROW_ERR_UV_EINVAL(js, "cp"_kj);
1323 }
1324 KJ_CASE_ONEOF(err, workerd::FsError) {
1325 throwFsError(js, err, "cp"_kj);
1326 }
1327 }
1328 KJ_UNREACHABLE;
1329 }
1330 
1331 // In this case, the destDir could not be opened, treat as an error.
1332 node::THROW_ERR_UV_EINVAL(js, "cp"_kj);
1333}
1334 
1335void handleCpFile(jsg::Lock& js,
1336 const workerd::VirtualFileSystem& vfs,
1337 kj::Rc<workerd::File> file,
1338 const jsg::Url& destUrl) {
1339 // Here, we are going to clone the file into a new file at the destination
1340 // if we are allowed to do so.
1341 // We need to check if the destination exists and is writable.
1342 auto relative = destUrl.getRelative();
1343 // relative.base is the parent directory path.
1344 // relative.name is the name of the link we are creating in the parent.
1345 
1346 auto basePath = kj::str(relative.base.getPathname().slice(1));
1347 kj::Path root{};
1348 auto base = root.eval(basePath);
1349 
1350 // We need to grab the parent directory, creating it if it does not exist
1351 // and we are permitted to do so.
1352 KJ_IF_SOME(destDir,
1353 vfs.getRoot(js)->tryOpen(js, base,
1354 {
1355 .createAs = workerd::FsType::DIRECTORY,
1356 .followLinks = true,
1357 })) {
1358 // Awesome, either the destination directory existed already or we
1359 // successfully created it, or an error was reported.
1360 KJ_SWITCH_ONEOF(destDir) {
1361 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1362 // We cannot copy into a file, so we throw an error.
1363 node::THROW_ERR_UV_ENOTDIR(js, "cp"_kj);
1364 }
1365 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1366 kj::Path path({relative.name});
1367 // This is the case we're looking for!
1368 // First, let's check to see if the target name already exists.
1369 // If it does, we'll remove it.
1370 KJ_SWITCH_ONEOF(dir->remove(js, path, {.recursive = false})) {
1371 KJ_CASE_ONEOF(err, workerd::FsError) {
1372 throwFsError(js, err, "cp"_kj);
1373 }
1374 KJ_CASE_ONEOF(b, bool) {
1375 // Ignore the return value, we don't actually care if the thing existed or not.
1376 }
1377 }
1378 // Now, we can add the file the directory.
1379 KJ_SWITCH_ONEOF(file->clone(js)) {
1380 KJ_CASE_ONEOF(err, workerd::FsError) {
1381 throwFsError(js, err, "cp"_kj);
1382 }
1383 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1384 KJ_IF_SOME(err, dir->add(js, relative.name, kj::mv(file))) {
1385 // If we got here, an error was reported
1386 throwFsError(js, err, "cp"_kj);
1387 }
1388 }
1389 }
1390 // If we got here, success!
1391 return;
1392 }
1393 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1394 // This shouldn't be possible since we told tryOpen to follow links.
1395 // But, let's just throw an error.
1396 node::THROW_ERR_UV_EINVAL(js, "cp"_kj);
1397 }
1398 KJ_CASE_ONEOF(err, workerd::FsError) {
1399 throwFsError(js, err, "cp"_kj);
1400 }
1401 }
1402 KJ_UNREACHABLE;
1403 }
1404 
1405 // In this case, the destDir could not be opened, treat as an error.
1406 node::THROW_ERR_UV_EINVAL(js, "cp"_kj);
1407}
1408 
1409void handleCpDir(jsg::Lock& js,
1410 const workerd::VirtualFileSystem& vfs,
1411 kj::Rc<workerd::Directory> src,
1412 kj::Rc<workerd::Directory> dest,
1413 const FileSystemModule::CpOptions& options) {
1414 auto stat = dest->stat(js);
1415 if (!stat.writable) {
1416 node::THROW_ERR_UV_EPERM(js, "cp"_kj, "Destination directory is not writable"_kj);
1417 }
1418 if (src.get() == dest.get()) {
1419 node::THROW_ERR_UV_EINVAL(js, "cp"_kj, "Source and destination directories are the same"_kj);
1420 }
1421 
1422 // Here, we iterate through each of the entries in the source directory,
1423 // recursively copying them to the destination directory.
1424 for (auto& entry: *src.get()) {
1425 kj::StringPtr name = entry.key;
1426 KJ_SWITCH_ONEOF(entry.value) {
1427 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1428 // We have a file, we will copy it to the destination directory
1429 // unless errorOnExist is true, force is false, and the destination already exists.
1430 
1431 KJ_IF_SOME(existing,
1432 dest->tryOpen(js, kj::Path({name}),
1433 {
1434 .followLinks = options.deferenceSymlinks,
1435 })) {
1436 // The destination path already exists. Check to see if we can overwrite it.
1437 KJ_SWITCH_ONEOF(existing) {
1438 KJ_CASE_ONEOF(existingFile, kj::Rc<workerd::File>) {
1439 if (existingFile.get() == file.get()) {
1440 // Do nothing
1441 } else if (options.force) {
1442 KJ_SWITCH_ONEOF(dest->remove(js, kj::Path({name}), {.recursive = false})) {
1443 KJ_CASE_ONEOF(err, workerd::FsError) {
1444 throwFsError(js, err, "cp"_kj);
1445 }
1446 KJ_CASE_ONEOF(b, bool) {
1447 // Ignore the return value.
1448 }
1449 }
1450 KJ_SWITCH_ONEOF(file->clone(js)) {
1451 KJ_CASE_ONEOF(err, workerd::FsError) {
1452 throwFsError(js, err, "cp"_kj);
1453 }
1454 KJ_CASE_ONEOF(cloned, kj::Rc<workerd::File>) {
1455 KJ_IF_SOME(err, dest->add(js, name, kj::mv(cloned))) {
1456 // If we got here, an error was reported
1457 throwFsError(js, err, "cp"_kj);
1458 }
1459 }
1460 }
1461 } else if (options.errorOnExist) {
1462 node::THROW_ERR_UV_EEXIST(
1463 js, "cp"_kj, kj::str("Destination already exists: ", name));
1464 }
1465 // If we got here, we are not overwriting the file, so we just ignore it.
1466 }
1467 KJ_CASE_ONEOF(existingDir, kj::Rc<workerd::Directory>) {
1468 // We cannot overwrite a directory with a file, so we throw an error.
1469 node::THROW_ERR_UV_EISDIR(
1470 js, "cp"_kj, kj::str("Cannot copy file to directory: ", name));
1471 }
1472 KJ_CASE_ONEOF(_, kj::Rc<workerd::SymbolicLink>) {
1473 // We're going to replace the existing link with the file.
1474 if (options.force) {
1475 KJ_SWITCH_ONEOF(dest->remove(js, kj::Path({name}), {.recursive = false})) {
1476 KJ_CASE_ONEOF(err, workerd::FsError) {
1477 throwFsError(js, err, "cp"_kj);
1478 }
1479 KJ_CASE_ONEOF(b, bool) {
1480 // Ignore the return value.
1481 }
1482 }
1483 KJ_SWITCH_ONEOF(file->clone(js)) {
1484 KJ_CASE_ONEOF(err, workerd::FsError) {
1485 throwFsError(js, err, "cp"_kj);
1486 }
1487 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1488 KJ_IF_SOME(err, dest->add(js, name, kj::mv(file))) {
1489 // If we got here, an error was reported
1490 throwFsError(js, err, "cp"_kj);
1491 }
1492 }
1493 }
1494 } else if (options.errorOnExist) {
1495 node::THROW_ERR_UV_EEXIST(
1496 js, "cp"_kj, kj::str("Destination already exists: ", name));
1497 }
1498 // If we got here, we are not overwriting the file, so we just ignore it.
1499 }
1500 KJ_CASE_ONEOF(err, workerd::FsError) {
1501 throwFsError(js, err, "cp"_kj);
1502 }
1503 }
1504 } else {
1505 KJ_SWITCH_ONEOF(file->clone(js)) {
1506 KJ_CASE_ONEOF(err, workerd::FsError) {
1507 throwFsError(js, err, "cp"_kj);
1508 }
1509 KJ_CASE_ONEOF(cloned, kj::Rc<workerd::File>) {
1510 KJ_IF_SOME(err, dest->add(js, name, kj::mv(cloned))) {
1511 // If we got here, an error was reported
1512 throwFsError(js, err, "cp"_kj);
1513 }
1514 }
1515 }
1516 }
1517 }
1518 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1519 // We have a directory, we will copy it to the destination directory
1520 // recursively.
1521 
1522 // First, we need to check if the destination directory already exists.
1523 KJ_IF_SOME(existing,
1524 dest->tryOpen(js, kj::Path({name}),
1525 {
1526 .followLinks = options.deferenceSymlinks,
1527 })) {
1528 // The destination exists. Check to see if we can overwrite it.
1529 KJ_SWITCH_ONEOF(existing) {
1530 KJ_CASE_ONEOF(existingFile, kj::Rc<workerd::File>) {
1531 // The destination is a file, we cannot overwrite it with a directory.
1532 node::THROW_ERR_UV_ENOTDIR(
1533 js, "cp"_kj, kj::str("Cannot copy directory to file: ", name));
1534 }
1535 KJ_CASE_ONEOF(existingDir, kj::Rc<workerd::Directory>) {
1536 handleCpDir(js, vfs, kj::mv(dir), kj::mv(existingDir), options);
1537 }
1538 KJ_CASE_ONEOF(existingLink, kj::Rc<workerd::SymbolicLink>) {
1539 // The destination is a symbolic link, we can overwrite it with a directory.
1540 node::THROW_ERR_UV_EISDIR(
1541 js, "cp"_kj, kj::str("Cannot copy directory to symbolic link: ", name));
1542 }
1543 KJ_CASE_ONEOF(err, workerd::FsError) {
1544 throwFsError(js, err, "cp"_kj);
1545 }
1546 }
1547 } else {
1548 // The destination does not exist, we'll need to create a new directory.
1549 // then recursively copy into it.
1550 auto newDir = workerd::Directory::newWritable(js);
1551 KJ_IF_SOME(err, dest->add(js, name, newDir.addRef())) {
1552 // If we got here, an error was reported
1553 throwFsError(js, err, "cp"_kj);
1554 }
1555 // Now we can recursively copy the contents of the source directory into the new one.
1556 handleCpDir(js, vfs, kj::mv(dir), kj::mv(newDir), options);
1557 }
1558 }
1559 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1560 KJ_IF_SOME(existing,
1561 dest->tryOpen(js, kj::Path({name}),
1562 {
1563 .followLinks = options.deferenceSymlinks,
1564 })) {
1565 // The destination path already exists. Check to see if we can overwrite it.
1566 KJ_SWITCH_ONEOF(existing) {
1567 KJ_CASE_ONEOF(_, kj::Rc<workerd::File>) {
1568 if (options.force) {
1569 KJ_SWITCH_ONEOF(dest->remove(js, kj::Path({name}), {.recursive = false})) {
1570 KJ_CASE_ONEOF(err, workerd::FsError) {
1571 throwFsError(js, err, "cp"_kj);
1572 }
1573 KJ_CASE_ONEOF(b, bool) {
1574 // Ignore the return value.
1575 }
1576 }
1577 KJ_IF_SOME(err, dest->add(js, name, link.addRef())) {
1578 // If we got here, an error was reported
1579 throwFsError(js, err, "cp"_kj);
1580 }
1581 } else if (options.errorOnExist) {
1582 node::THROW_ERR_UV_EEXIST(
1583 js, "cp"_kj, kj::str("Destination already exists: ", name));
1584 }
1585 // If we got here, we are not overwriting the file, so we just ignore it.
1586 }
1587 KJ_CASE_ONEOF(existingDir, kj::Rc<workerd::Directory>) {
1588 // We cannot overwrite a directory with a file, so we throw an error.
1589 node::THROW_ERR_UV_EISDIR(
1590 js, "cp"_kj, kj::str("Cannot copy link to directory: ", name));
1591 }
1592 KJ_CASE_ONEOF(existingLink, kj::Rc<workerd::SymbolicLink>) {
1593 if (existingLink.get() == link.get()) {
1594 // Do nothing
1595 } else if (options.force) {
1596 KJ_SWITCH_ONEOF(dest->remove(js, kj::Path({name}), {.recursive = false})) {
1597 KJ_CASE_ONEOF(err, workerd::FsError) {
1598 throwFsError(js, err, "cp"_kj);
1599 }
1600 KJ_CASE_ONEOF(b, bool) {
1601 // Ignore the return value.
1602 }
1603 }
1604 KJ_IF_SOME(err, dest->add(js, name, link.addRef())) {
1605 // If we got here, an error was reported
1606 throwFsError(js, err, "cp"_kj);
1607 }
1608 } else if (options.errorOnExist) {
1609 node::THROW_ERR_UV_EEXIST(
1610 js, "cp"_kj, kj::str("Destination already exists: ", name));
1611 }
1612 // If we got here, we are not overwriting the file, so we just ignore it.
1613 }
1614 KJ_CASE_ONEOF(err, workerd::FsError) {
1615 throwFsError(js, err, "cp"_kj);
1616 }
1617 }
1618 } else KJ_IF_SOME(err, dest->add(js, name, link.addRef())) {
1619 // If we got here, an error was reported
1620 throwFsError(js, err, "cp"_kj);
1621 }
1622 }
1623 }
1624 }
1625}
1626 
1627void handleCpDir(jsg::Lock& js,
1628 const workerd::VirtualFileSystem& vfs,
1629 kj::Rc<workerd::Directory> src,
1630 const jsg::Url& dest,
1631 const FileSystemModule::CpOptions& options) {
1632 // For this variation of handleCpDir, the dest needs to be created as a directory.
1633 // The assumption here is that the destination does not yet exist. Let's create it.
1634 
1635 auto basePath = kj::str(dest.getPathname().slice(1));
1636 kj::Path root{};
1637 auto path = root.eval(basePath);
1638 
1639 KJ_IF_SOME(destDir,
1640 vfs.getRoot(js)->tryOpen(js, path,
1641 {
1642 .createAs = workerd::FsType::DIRECTORY,
1643 .followLinks = true,
1644 })) {
1645 KJ_SWITCH_ONEOF(destDir) {
1646 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1647 // We cannot copy into a file, so we throw an error.
1648 node::THROW_ERR_UV_ENOTDIR(js, "cp"_kj);
1649 }
1650 KJ_CASE_ONEOF(destination, kj::Rc<workerd::Directory>) {
1651 // Nice... we have our destination directory. Continue to copy the contents.
1652 return handleCpDir(js, vfs, kj::mv(src), kj::mv(destination), options);
1653 }
1654 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1655 // This shouldn't be possible since we told tryOpen to follow links.
1656 // But, let's just throw an error.
1657 node::THROW_ERR_UV_EINVAL(js, "cp"_kj);
1658 }
1659 KJ_CASE_ONEOF(err, workerd::FsError) {
1660 throwFsError(js, err, "cp"_kj);
1661 }
1662 }
1663 KJ_UNREACHABLE;
1664 }
1665 
1666 // If we got here, then for some reason we could not open/create the destination
1667 // directory. Since we passed createAs, we shouldn't really be able to get here.
1668 node::THROW_ERR_UV_EINVAL(js, "cp"_kj);
1669}
1670 
1671void cpImpl(jsg::Lock& js,
1672 const workerd::VirtualFileSystem& vfs,
1673 const jsg::Url& src,
1674 const jsg::Url& dest,
1675 const FileSystemModule::CpOptions& options) {
1676 
1677 // Cannot copy a file to itself.
1678 JSG_REQUIRE(!src.equal(dest,
1679 jsg::Url::EquivalenceOption::IGNORE_FRAGMENTS |
1680 jsg::Url::EquivalenceOption::IGNORE_SEARCH |
1681 jsg::Url::EquivalenceOption::NORMALIZE_PATH),
1682 Error, "Source and destination paths must not be the same"_kj);
1683 
1684 // Cannot copy a directory to a subdirectory of itself. The pathnames are
1685 // already normalized by jsg::Url (no .., //, or trailing slashes except root).
1686 {
1687 auto srcPath = src.getPathname();
1688 auto destPath = dest.getPathname();
1689 KJ_ASSERT(srcPath.size() > 0, "normalized URL pathname must not be empty");
1690 auto srcPrefix = srcPath.back() == '/' ? kj::str(srcPath) : kj::str(srcPath, "/");
1691 auto destStr = kj::StringPtr(destPath.begin(), destPath.size());
1692 if (destStr.size() > srcPrefix.size() && destStr.startsWith(srcPrefix)) {
1693 node::THROW_ERR_FS_CP_EINVAL(
1694 js, kj::str("Cannot copy '", srcPath, "' to a subdirectory of itself, '", destPath, "'"));
1695 }
1696 }
1697 
1698 // Step 1: If deferenceSymlinks is true, the we will be following symbolic links. If
1699 // it is false, we won't be.
1700 
1701 auto maybeSrcNode = getNodeOrError(js, vfs, src, options);
1702 auto maybeDestNode = getNodeOrError(js, vfs, dest, options);
1703 
1704 KJ_IF_SOME(sourceNode, maybeSrcNode) {
1705 KJ_SWITCH_ONEOF(sourceNode) {
1706 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1707 KJ_IF_SOME(node, maybeDestNode) {
1708 KJ_SWITCH_ONEOF(node) {
1709 KJ_CASE_ONEOF(_, kj::Rc<workerd::File>) {
1710 // If options.force is true, we will overwrite the destination file.
1711 if (options.force) {
1712 return handleCpFile(js, vfs, kj::mv(file), dest);
1713 }
1714 // Otherwise, if options.errorOnExist is true, we will throw an error.
1715 if (options.errorOnExist) {
1716 node::THROW_ERR_FS_CP_EEXIST(js);
1717 }
1718 // Otherwise, we skip this file and do nothing.
1719 return;
1720 }
1721 KJ_CASE_ONEOF(_, kj::Rc<workerd::Directory>) {
1722 // Simple case: user is trying to copy a file over a directory
1723 // which is not allowed.
1724 node::THROW_ERR_FS_CP_NON_DIR_TO_DIR(js);
1725 }
1726 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1727 if (options.deferenceSymlinks) {
1728 KJ_IF_SOME(target, link->resolve(js)) {
1729 KJ_SWITCH_ONEOF(target) {
1730 KJ_CASE_ONEOF(targetFile, kj::Rc<workerd::File>) {
1731 KJ_SWITCH_ONEOF(file->clone(js)) {
1732 KJ_CASE_ONEOF(err, workerd::FsError) {
1733 throwFsError(js, err, "cp"_kj);
1734 }
1735 KJ_CASE_ONEOF(clonedFile, kj::Rc<workerd::File>) {
1736 KJ_IF_SOME(err, targetFile->replace(js, kj::mv(clonedFile))) {
1737 throwFsError(js, err, "cp"_kj);
1738 }
1739 }
1740 }
1741 return;
1742 }
1743 KJ_CASE_ONEOF(_, kj::Rc<workerd::Directory>) {
1744 node::THROW_ERR_UV_EISDIR(js, "cp"_kj);
1745 }
1746 KJ_CASE_ONEOF(err, workerd::FsError) {
1747 throwFsError(js, err, "cp"_kj);
1748 }
1749 }
1750 } else {
1751 node::THROW_ERR_UV_ENOENT(
1752 js, "cp"_kj, nullptr, kj::str(link->getTargetUrl().getPathname()));
1753 }
1754 }
1755 
1756 // We would only get here if deferenceSymlinks is false.
1757 // In this case, if errorOnExist is true and force is false,
1758 // we will throw an error.
1759 if (options.force) {
1760 // Copy the file contents to the destination, replacing
1761 // the symbolic link with a copy of the file.
1762 return handleCpFile(js, vfs, kj::mv(file), dest);
1763 }
1764 if (options.errorOnExist) {
1765 node::THROW_ERR_FS_CP_EEXIST(js);
1766 }
1767 
1768 // Otherwise, we skip this file and do nothing.
1769 return;
1770 }
1771 }
1772 KJ_UNREACHABLE;
1773 }
1774 
1775 // Yay! we can just copy the file contents to the destination.
1776 // If the path to the destination does not exist, we will create it
1777 // if possible.
1778 return handleCpFile(js, vfs, kj::mv(file), dest);
1779 }
1780 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1781 // The source is a directory. The options.recursive option must be set
1782 // to true or we will fail.
1783 if (!options.recursive) {
1784 node::THROW_ERR_FS_EISDIR(js);
1785 }
1786 
1787 KJ_IF_SOME(dest, maybeDestNode) {
1788 KJ_SWITCH_ONEOF(dest) {
1789 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1790 // Simple case: user is trying to copy a directory over a file
1791 // which is not allowed.
1792 node::THROW_ERR_FS_CP_DIR_TO_NON_DIR(js);
1793 }
1794 KJ_CASE_ONEOF(destDir, kj::Rc<workerd::Directory>) {
1795 // So Node.js has a bit of an inconsistency here when copying directories.
1796 // When copying a file over a file, we will check the errorOnExist and force
1797 // options, failing if the destination file exists and errorOnExist is true,
1798 // unless the force option is set. If both are false, we skip the copy.
1799 // However, the same logic is not applied to copying a directory. If the
1800 // destination directory exists, we will still proceed to copy the source
1801 // directory into the destination directory, only applying the force and
1802 // errorOnExist options to individual files within the directories.
1803 // See: https://github.com/nodejs/node/issues/58947
1804 return handleCpDir(js, vfs, kj::mv(dir), kj::mv(destDir), options);
1805 }
1806 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1807 // Also a simple case, user is trying to copy a directory over
1808 // an existing symbolic link, which we do not allow.
1809 node::THROW_ERR_UV_ENOTDIR(js, "cp"_kj);
1810 }
1811 }
1812 KJ_UNREACHABLE;
1813 }
1814 
1815 // Yay! we can just copy the file contents to the destination.
1816 // If the path to the destination does not exist, we will create it
1817 // if possible.
1818 return handleCpDir(js, vfs, kj::mv(dir), dest, options);
1819 }
1820 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1821 // If we got here, then the source is itself a symbolic link.
1822 // The destination, if we do copy it, will also be a symbolic link to
1823 // the same target. The options.errorOnExist and options.force still
1824 // apply here, but we will not follow the symbolic link at all.
1825 KJ_IF_SOME(node, maybeDestNode) {
1826 KJ_SWITCH_ONEOF(node) {
1827 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1828 if (options.force) {
1829 return handleCpLink(js, vfs, kj::mv(link), dest);
1830 }
1831 
1832 if (options.errorOnExist) {
1833 node::THROW_ERR_FS_CP_EEXIST(js);
1834 }
1835 
1836 // Otherwise we skip this file and do nothing.
1837 return;
1838 }
1839 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1840 // Simple case: user is trying to copy a symbolic link over a directory
1841 // which is not allowed.
1842 node::THROW_ERR_UV_ENOTDIR(js, "cp"_kj);
1843 }
1844 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1845 if (options.force) {
1846 return handleCpLink(js, vfs, kj::mv(link), dest);
1847 }
1848 
1849 if (options.errorOnExist) {
1850 node::THROW_ERR_FS_CP_EEXIST(js);
1851 }
1852 
1853 // Otherwise we skip this file and do nothing.
1854 return;
1855 }
1856 }
1857 KJ_UNREACHABLE;
1858 }
1859 
1860 // Yay! we can just copy fhe symbolic link to the destination.
1861 // If the path to the destination does not exist, we will create it
1862 // if possible.
1863 return handleCpLink(js, vfs, kj::mv(link), dest);
1864 }
1865 }
1866 KJ_UNREACHABLE;
1867 }
1868 
1869 // If we got here, the sourceNode does not exist.
1870 node::THROW_ERR_UV_ENOENT(js, "cp"_kj, nullptr, kj::str(src.getPathname()));
1871}
1872} // namespace
1873 
1874void FileSystemModule::cp(jsg::Lock& js, FilePath src, FilePath dest, CpOptions options) {
1875 auto& vfs = workerd::VirtualFileSystem::current(js);
1876 NormalizedFilePath normalizedSrc(kj::mv(src));
1877 NormalizedFilePath normalizedDest(kj::mv(dest));
1878 // TODO(node-fs): Support the preserveTimestamps option.
1879 cpImpl(js, vfs, normalizedSrc, normalizedDest, options);
1880}
1881 
1882jsg::Ref<Blob> FileSystemModule::openAsBlob(
1883 jsg::Lock& js, FilePath path, OpenAsBlobOptions options) {
1884 auto& vfs = workerd::VirtualFileSystem::current(js);
1885 NormalizedFilePath normalizedSrc(kj::mv(path));
1886 KJ_IF_SOME(item, vfs.resolve(js, normalizedSrc, {})) {
1887 KJ_SWITCH_ONEOF(item) {
1888 KJ_CASE_ONEOF(err, workerd::FsError) {
1889 node::THROW_ERR_UV_ENOENT(js, "open"_kj, nullptr, kj::str(normalizedSrc.url.getPathname()));
1890 }
1891 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
1892 KJ_SWITCH_ONEOF(file->readAllBytes(js)) {
1893 KJ_CASE_ONEOF(bytes, jsg::BufferSource) {
1894 return js.alloc<Blob>(
1895 js, bytes.getJsHandle(js), kj::mv(options.type).orDefault(kj::String()));
1896 }
1897 KJ_CASE_ONEOF(err, workerd::FsError) {
1898 throwFsError(js, err, "open"_kj);
1899 }
1900 }
1901 KJ_UNREACHABLE;
1902 }
1903 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
1904 node::THROW_ERR_UV_EISDIR(js, "open"_kj);
1905 }
1906 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
1907 node::THROW_ERR_UV_EINVAL(js, "open"_kj);
1908 }
1909 }
1910 KJ_UNREACHABLE;
1911 }
1912 
1913 node::THROW_ERR_UV_ENOENT(js, "open"_kj, nullptr, kj::str(normalizedSrc.url.getPathname()));
1914}
1915 
1916// =======================================================================================
1917 
1918jsg::Ref<FileFdHandle> FileFdHandle::constructor(jsg::Lock& js, int fd) {
1919 auto& vfs = workerd::VirtualFileSystem::current(js);
1920 return js.alloc<FileFdHandle>(js, vfs, fd);
1921}
1922 
1923FileFdHandle::FileFdHandle(jsg::Lock& js, const workerd::VirtualFileSystem& vfs, int fd)
1924 : fdHandle(vfs.wrapFd(js, fd)),
1925 isolate(js.v8Isolate) {}
1926 
1927void FileFdHandle::close(jsg::Lock& js) {
1928 fdHandle = kj::none;
1929}
1930 
1931FileFdHandle::~FileFdHandle() noexcept {
1932 // In Node.js, closing the file descriptor on destruction is an
1933 // error (it was a deprecated behavior for a long time and
1934 // was recently upgraded to a catchable error in Node.js. However,
1935 // throwing an error in our implementation is of questionable value
1936 // since it's not clear exactly what the user is supposed to do about
1937 // it beyond making sure to explicitly close the file descriptor
1938 // before the object is destroyed, which we can't guarantee.
1939 KJ_IF_SOME(handle, fdHandle) {
1940 if (isolate != nullptr) {
1941 if (v8::Locker::IsLocked(isolate)) {
1942 // If the isolate is locked, we can safely close the fdHandle.
1943 // This is because closing the handle requires decrementing the
1944 // refcount on the underlying node object, which can only be done
1945 // safely while the isolate is locked.
1946 fdHandle = kj::none;
1947 } else {
1948 // If the isolate is not locked, we cannot safely close the fdHandle
1949 // yet. We need to make sure the isolate is locked before we close it.
1950 // So instead, we will defer the actual destruction to when we next
1951 // hold the isolate lock.
1952 jsg::IsolateBase::from(isolate).destroyUnderLock(kj::mv(handle));
1953 fdHandle = kj::none;
1954 }
1955 }
1956 
1957 // If we have an active IoContext, then we'll go ahead and log a warning.
1958 KJ_IF_SOME(ioContext, IoContext::tryCurrent()) {
1959 ioContext.logWarning("A FileHandle was destroyed without being closed. This is "
1960 "not recommended and may lead to file descriptors being held "
1961 "far longer than necessary. Please make sure to explicitly close "
1962 "the FileHandle object explicitly before it is destroyed."_kj);
1963 }
1964 }
1965}
1966 
1967// =======================================================================================
1968// Implementation of the Web File System API
1969 
1970namespace {
1971constexpr bool isValidFileName(kj::StringPtr name) {
1972 return name.size() > 0 && name != "."_kj && name != ".."_kj && name.find("/"_kj) == kj::none &&
1973 name.find("\\"_kj) == kj::none;
1974}
1975 
1976jsg::Ref<jsg::DOMException> fsErrorToDomException(jsg::Lock& js, workerd::FsError error) {
1977 switch (error) {
1978 case workerd::FsError::NOT_DIRECTORY: {
1979 return js.domException(kj::str("NotSupportedError"), kj::str("Not a directory"));
1980 }
1981 case workerd::FsError::NOT_EMPTY: {
1982 return js.domException(kj::str("InvalidModificationError"), kj::str("Directory not empty"));
1983 }
1984 case workerd::FsError::READ_ONLY: {
1985 return js.domException(kj::str("InvalidStateError"), kj::str("Read-only file system"));
1986 }
1987 case workerd::FsError::NOT_PERMITTED: {
1988 return js.domException(kj::str("NotAllowedError"), kj::str("Operation not permitted"));
1989 }
1990 case workerd::FsError::NOT_PERMITTED_ON_DIRECTORY: {
1991 return js.domException(
1992 kj::str("NotAllowedError"), kj::str("Operation not permitted on a directory"));
1993 }
1994 case workerd::FsError::ALREADY_EXISTS: {
1995 return js.domException(kj::str("InvalidStateError"), kj::str("File already exists"));
1996 }
1997 case workerd::FsError::TOO_MANY_OPEN_FILES: {
1998 return js.domException(kj::str("QuotaExceededError"),
1999 kj::str("Too many open files, please close some files and try again"));
2000 }
2001 case workerd::FsError::FAILED: {
2002 return js.domException(kj::str("UnknownError"), kj::str("File system operation failed"));
2003 }
2004 case workerd::FsError::NOT_SUPPORTED: {
2005 return js.domException(kj::str("NotSupportedError"), kj::str("Operation not supported"));
2006 }
2007 case workerd::FsError::INVALID_PATH: {
2008 return js.domException(kj::str("TypeMismatchError"), kj::str("Invalid file path"));
2009 }
2010 case workerd::FsError::FILE_SIZE_LIMIT_EXCEEDED: {
2011 return js.domException(kj::str("QuotaExceededError"),
2012 kj::str("File size limit exceeded, please reduce the file size and try again"));
2013 }
2014 case workerd::FsError::SYMLINK_DEPTH_EXCEEDED: {
2015 return js.domException(kj::str("InvalidStateError"),
2016 kj::str("Symbolic link depth exceeded, please check the symbolic links"));
2017 }
2018 default: {
2019 return js.domException(
2020 kj::str("UnknownError"), kj::str("Unknown file system error: ", static_cast<int>(error)));
2021 }
2022 }
2023 KJ_UNREACHABLE;
2024}
2025} // namespace
2026 
2027FileSystemHandle::FileSystemHandle(
2028 const workerd::VirtualFileSystem& vfs, jsg::Url&& locator, jsg::USVString name)
2029 : vfs(vfs),
2030 locator(kj::mv(locator)),
2031 name(kj::mv(name)) {}
2032 
2033jsg::Promise<kj::StringPtr> FileSystemHandle::getUniqueId(
2034 jsg::Lock& js, const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& deHandler) {
2035 KJ_IF_SOME(item, vfs.resolve(js, getLocator(), {})) {
2036 KJ_SWITCH_ONEOF(item) {
2037 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2038 return js.resolvedPromise(file->getUniqueId(js));
2039 }
2040 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2041 return js.resolvedPromise(dir->getUniqueId(js));
2042 }
2043 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2044 return js.resolvedPromise(link->getUniqueId(js));
2045 }
2046 KJ_CASE_ONEOF(err, workerd::FsError) {
2047 return js.rejectedPromise<kj::StringPtr>(
2048 deHandler.wrap(js, fsErrorToDomException(js, err)));
2049 }
2050 }
2051 KJ_UNREACHABLE;
2052 }
2053 auto ex = js.domException(kj::str("NotFoundError"), kj::str("The entry was not found."));
2054 return js.rejectedPromise<kj::StringPtr>(deHandler.wrap(js, kj::mv(ex)));
2055}
2056 
2057jsg::Promise<bool> FileSystemHandle::isSameEntry(jsg::Lock& js, jsg::Ref<FileSystemHandle> other) {
2058 // Per the spec, two handles are the same if they refer to the same entry (that is,
2059 // have the same locator). It does not matter if they are different actual entries.
2060 return getKind(js) == other->getKind(js) &&
2061 locator.equal(other->getLocator(),
2062 jsg::Url::EquivalenceOption::IGNORE_FRAGMENTS |
2063 jsg::Url::EquivalenceOption::IGNORE_SEARCH |
2064 jsg::Url::EquivalenceOption::NORMALIZE_PATH)
2065 ? js.resolvedPromise(true)
2066 : js.resolvedPromise(false);
2067}
2068 
2069jsg::Promise<void> FileSystemHandle::remove(jsg::Lock& js,
2070 jsg::Optional<RemoveOptions> options,
2071 const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& deHandler) {
2072 
2073 if (!canBeModifiedCurrently(js)) {
2074 auto ex = js.domException(kj::str("NoModificationAllowedError"),
2075 kj::str("Cannot remove a handle that is not writable or not a directory."));
2076 return js.rejectedPromise<void>(deHandler.wrap(js, kj::mv(ex)));
2077 }
2078 
2079 auto relative = getLocator().getRelative(jsg::Url::RelativeOption::STRIP_TAILING_SLASHES);
2080 auto opts = options.orDefault(RemoveOptions{});
2081 auto recursive = opts.recursive.orDefault(false);
2082 KJ_IF_SOME(parent, vfs.resolve(js, relative.base, workerd::VirtualFileSystem::ResolveOptions{})) {
2083 KJ_SWITCH_ONEOF(parent) {
2084 KJ_CASE_ONEOF(parentDir, kj::Rc<workerd::Directory>) {
2085 // Webfs requires that the entry exists before we try to remove it.
2086 kj::Path path({name});
2087 if (parentDir->stat(js, path) == kj::none) {
2088 auto ex = js.domException(kj::str("NotFoundError"), kj::str("The entry was not found."));
2089 return js.rejectedPromise<void>(deHandler.wrap(js, kj::mv(ex)));
2090 }
2091 
2092 KJ_SWITCH_ONEOF(parentDir->remove(js, path, {.recursive = recursive})) {
2093 KJ_CASE_ONEOF(err, workerd::FsError) {
2094 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2095 }
2096 KJ_CASE_ONEOF(removed, bool) {
2097 if (!removed) {
2098 auto ex =
2099 js.domException(kj::str("NotFoundError"), kj::str("The entry was not found."));
2100 return js.rejectedPromise<void>(deHandler.wrap(js, kj::mv(ex)));
2101 }
2102 return js.resolvedPromise();
2103 }
2104 }
2105 }
2106 KJ_CASE_ONEOF(_, kj::Rc<workerd::File>) {
2107 return js.rejectedPromise<void>(
2108 deHandler.wrap(js, fsErrorToDomException(js, workerd::FsError::NOT_DIRECTORY)));
2109 }
2110 KJ_CASE_ONEOF(_, kj::Rc<workerd::SymbolicLink>) {
2111 return js.rejectedPromise<void>(
2112 deHandler.wrap(js, fsErrorToDomException(js, workerd::FsError::NOT_DIRECTORY)));
2113 }
2114 KJ_CASE_ONEOF(err, workerd::FsError) {
2115 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2116 }
2117 }
2118 KJ_UNREACHABLE;
2119 }
2120 auto ex = js.domException(kj::str("NotFoundError"), kj::str("The entry was not found."));
2121 return js.rejectedPromise<void>(deHandler.wrap(js, kj::mv(ex)));
2122}
2123 
2124bool FileSystemHandle::canBeModifiedCurrently(jsg::Lock& js) const {
2125 auto pathname = getLocator().getPathname();
2126 if (pathname.endsWith("/"_kj)) {
2127 auto cloned = getLocator().clone();
2128 cloned.setPathname(pathname.slice(0, pathname.size() - 1));
2129 return !getVfs().isLocked(js, cloned);
2130 }
2131 return !getVfs().isLocked(js, getLocator());
2132}
2133 
2134jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> StorageManager::getDirectory(
2135 jsg::Lock& js, const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
2136 auto& vfs = workerd::VirtualFileSystem::current(js);
2137 return js.resolvedPromise(js.alloc<FileSystemDirectoryHandle>(
2138 vfs, KJ_ASSERT_NONNULL(jsg::Url::tryParse("file:///"_kj)), jsg::USVString()));
2139}
2140 
2141FileSystemDirectoryHandle::FileSystemDirectoryHandle(
2142 const workerd::VirtualFileSystem& vfs, jsg::Url locator, jsg::USVString name)
2143 : FileSystemHandle(vfs, kj::mv(locator), kj::mv(name)) {}
2144 
2145jsg::Promise<jsg::Ref<FileSystemFileHandle>> FileSystemDirectoryHandle::getFileHandle(jsg::Lock& js,
2146 jsg::USVString name,
2147 jsg::Optional<FileSystemGetFileOptions> options,
2148 const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
2149 if (!isValidFileName(name)) {
2150 return js.rejectedPromise<jsg::Ref<FileSystemFileHandle>>(js.typeError("Invalid file name"));
2151 }
2152 kj::Maybe<FsType> createAs;
2153 KJ_IF_SOME(opts, options) {
2154 if (opts.create) createAs = FsType::FILE;
2155 }
2156 
2157 KJ_IF_SOME(existing, getVfs().resolve(js, getLocator(), {})) {
2158 KJ_SWITCH_ONEOF(existing) {
2159 KJ_CASE_ONEOF(err, workerd::FsError) {
2160 return js.rejectedPromise<jsg::Ref<FileSystemFileHandle>>(
2161 exception.wrap(js, fsErrorToDomException(js, err)));
2162 }
2163 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2164 auto locator = KJ_ASSERT_NONNULL(getLocator().tryResolve(name));
2165 auto relative = locator.getRelative();
2166 KJ_IF_SOME(node,
2167 dir->tryOpen(js, kj::Path({relative.name}),
2168 Directory::OpenOptions{
2169 .createAs = createAs,
2170 })) {
2171 KJ_SWITCH_ONEOF(node) {
2172 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2173 return js.resolvedPromise(
2174 js.alloc<FileSystemFileHandle>(getVfs(), kj::mv(locator), kj::mv(name)));
2175 }
2176 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2177 auto ex = js.domException(
2178 kj::str("TypeMismatchError"), kj::str("File name is a directory"));
2179 return js.rejectedPromise<jsg::Ref<FileSystemFileHandle>>(
2180 exception.wrap(js, kj::mv(ex)));
2181 }
2182 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2183 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Not a file"));
2184 return js.rejectedPromise<jsg::Ref<FileSystemFileHandle>>(
2185 exception.wrap(js, kj::mv(ex)));
2186 }
2187 KJ_CASE_ONEOF(err, workerd::FsError) {
2188 return js.rejectedPromise<jsg::Ref<FileSystemFileHandle>>(
2189 exception.wrap(js, fsErrorToDomException(js, err)));
2190 }
2191 }
2192 KJ_UNREACHABLE;
2193 }
2194 
2195 auto ex = js.domException(kj::str("NotFoundError"), kj::str("Not found"));
2196 return js.rejectedPromise<jsg::Ref<FileSystemFileHandle>>(exception.wrap(js, kj::mv(ex)));
2197 }
2198 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2199 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Not a directory"));
2200 return js.rejectedPromise<jsg::Ref<FileSystemFileHandle>>(exception.wrap(js, kj::mv(ex)));
2201 }
2202 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2203 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Not a directory"));
2204 return js.rejectedPromise<jsg::Ref<FileSystemFileHandle>>(exception.wrap(js, kj::mv(ex)));
2205 }
2206 }
2207 KJ_UNREACHABLE;
2208 }
2209 
2210 auto ex = js.domException(kj::str("NotFoundError"), kj::str("Directory not found"));
2211 return js.rejectedPromise<jsg::Ref<FileSystemFileHandle>>(exception.wrap(js, kj::mv(ex)));
2212}
2213 
2214jsg::Promise<jsg::Ref<FileSystemDirectoryHandle>> FileSystemDirectoryHandle::getDirectoryHandle(
2215 jsg::Lock& js,
2216 jsg::USVString name,
2217 jsg::Optional<FileSystemGetDirectoryOptions> options,
2218 const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
2219 if (!isValidFileName(name)) {
2220 return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(
2221 js.typeError("Invalid directory name"));
2222 }
2223 
2224 kj::Maybe<FsType> createAs;
2225 KJ_IF_SOME(opts, options) {
2226 if (opts.create) createAs = FsType::DIRECTORY;
2227 }
2228 
2229 KJ_IF_SOME(existing, getVfs().resolve(js, getLocator(), {})) {
2230 KJ_SWITCH_ONEOF(existing) {
2231 KJ_CASE_ONEOF(err, workerd::FsError) {
2232 return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(
2233 exception.wrap(js, fsErrorToDomException(js, err)));
2234 }
2235 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2236 auto locator = KJ_ASSERT_NONNULL(getLocator().tryResolve(name));
2237 auto relative = locator.getRelative();
2238 KJ_IF_SOME(node,
2239 dir->tryOpen(js, kj::Path({relative.name}),
2240 Directory::OpenOptions{
2241 .createAs = createAs,
2242 })) {
2243 KJ_SWITCH_ONEOF(node) {
2244 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2245 return js.resolvedPromise(js.alloc<FileSystemDirectoryHandle>(getVfs(),
2246 KJ_ASSERT_NONNULL(locator.resolve(kj::str(locator.getPathname(), "/"))),
2247 kj::mv(name)));
2248 }
2249 KJ_CASE_ONEOF(dir, kj::Rc<workerd::File>) {
2250 auto ex =
2251 js.domException(kj::str("TypeMismatchError"), kj::str("File name is a file"));
2252 return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(
2253 exception.wrap(js, kj::mv(ex)));
2254 }
2255 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2256 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Not a directory"));
2257 return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(
2258 exception.wrap(js, kj::mv(ex)));
2259 }
2260 KJ_CASE_ONEOF(err, workerd::FsError) {
2261 return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(
2262 exception.wrap(js, fsErrorToDomException(js, err)));
2263 }
2264 }
2265 KJ_UNREACHABLE;
2266 }
2267 // Could not open or create the directory.
2268 auto ex =
2269 js.domException(kj::str("NotFoundError"), kj::str("Directory not opened or created"));
2270 return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(
2271 exception.wrap(js, kj::mv(ex)));
2272 }
2273 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2274 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Not a directory"));
2275 return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(
2276 exception.wrap(js, kj::mv(ex)));
2277 }
2278 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2279 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Not a directory"));
2280 return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(
2281 exception.wrap(js, kj::mv(ex)));
2282 }
2283 }
2284 KJ_UNREACHABLE;
2285 }
2286 
2287 auto ex = js.domException(kj::str("NotFoundError"), kj::str("Directory not found"));
2288 return js.rejectedPromise<jsg::Ref<FileSystemDirectoryHandle>>(exception.wrap(js, kj::mv(ex)));
2289}
2290 
2291jsg::Promise<void> FileSystemDirectoryHandle::removeEntry(jsg::Lock& js,
2292 jsg::USVString name,
2293 jsg::Optional<FileSystemRemoveOptions> options,
2294 const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
2295 if (!isValidFileName(name)) {
2296 return js.rejectedPromise<void>(js.typeError("Invalid name"));
2297 }
2298 auto opts = options.orDefault(FileSystemRemoveOptions{});
2299 
2300 KJ_IF_SOME(existing, getVfs().resolve(js, getLocator(), {})) {
2301 KJ_SWITCH_ONEOF(existing) {
2302 KJ_CASE_ONEOF(err, workerd::FsError) {
2303 return js.rejectedPromise<void>(exception.wrap(js, fsErrorToDomException(js, err)));
2304 }
2305 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2306 kj::Path item({name});
2307 auto fileLocator = KJ_ASSERT_NONNULL(getLocator().tryResolve(name));
2308 if (getVfs().isLocked(js, fileLocator)) {
2309 // If the file is locked, we cannot remove it.
2310 auto ex = js.domException(kj::str("NoModificationAllowedError"),
2311 kj::str("Cannot remove an entry that is currently locked."));
2312 return js.rejectedPromise<void>(exception.wrap(js, kj::mv(ex)));
2313 }
2314 
2315 KJ_SWITCH_ONEOF(dir->remove(js, item,
2316 workerd::Directory::RemoveOptions{
2317 .recursive = opts.recursive,
2318 })) {
2319 KJ_CASE_ONEOF(res, bool) {
2320 if (res) {
2321 return js.resolvedPromise();
2322 }
2323 // If the entry was not found, we throw a NotFoundError.
2324 auto ex = js.domException(kj::str("NotFoundError"), kj::str("File not found"));
2325 return js.rejectedPromise<void>(exception.wrap(js, kj::mv(ex)));
2326 }
2327 KJ_CASE_ONEOF(error, workerd::FsError) {
2328 return js.rejectedPromise<void>(exception.wrap(js, fsErrorToDomException(js, error)));
2329 }
2330 }
2331 KJ_UNREACHABLE;
2332 }
2333 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2334 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Not a directory"));
2335 return js.rejectedPromise<void>(exception.wrap(js, kj::mv(ex)));
2336 }
2337 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2338 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Not a directory"));
2339 return js.rejectedPromise<void>(exception.wrap(js, kj::mv(ex)));
2340 }
2341 }
2342 KJ_UNREACHABLE;
2343 }
2344 
2345 auto ex = js.domException(kj::str("NotFoundError"), kj::str("Not found"));
2346 return js.rejectedPromise<void>(exception.wrap(js, kj::mv(ex)));
2347}
2348 
2349jsg::Promise<kj::Array<jsg::USVString>> FileSystemDirectoryHandle::resolve(
2350 jsg::Lock& js, jsg::Ref<FileSystemHandle> possibleDescendant) {
2351 JSG_FAIL_REQUIRE(Error, "Not implemented");
2352}
2353 
2354namespace {
2355kj::Array<jsg::Ref<FileSystemHandle>> collectEntries(const workerd::VirtualFileSystem& vfs,
2356 jsg::Lock& js,
2357 kj::Rc<workerd::Directory> inner,
2358 const jsg::Url& parentLocator) {
2359 kj::Vector<jsg::Ref<FileSystemHandle>> entries;
2360 for (auto& entry: *inner.get()) {
2361 KJ_SWITCH_ONEOF(entry.value) {
2362 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2363 auto locator = KJ_ASSERT_NONNULL(parentLocator.tryResolve(entry.key));
2364 entries.add(js.alloc<FileSystemFileHandle>(
2365 vfs, kj::mv(locator), jsg::USVString(kj::str(entry.key))));
2366 }
2367 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2368 auto locator = KJ_ASSERT_NONNULL(parentLocator.tryResolve(kj::str(entry.key, "/")));
2369 entries.add(js.alloc<FileSystemDirectoryHandle>(
2370 vfs, kj::mv(locator), jsg::USVString(kj::str(entry.key))));
2371 }
2372 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2373 SymbolicLinkRecursionGuardScope guardScope;
2374 KJ_IF_SOME(_, guardScope.checkSeen(link.get())) {
2375 // Throw a DOMException indicating that the symbolic link is recursive.
2376 JSG_FAIL_REQUIRE(DOMOperationError, "Symbolic link recursion detected"_kj);
2377 }
2378 KJ_IF_SOME(res, link->resolve(js)) {
2379 KJ_SWITCH_ONEOF(res) {
2380 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2381 auto locator = KJ_ASSERT_NONNULL(parentLocator.tryResolve(entry.key));
2382 entries.add(js.alloc<FileSystemFileHandle>(
2383 vfs, kj::mv(locator), jsg::USVString(kj::str(entry.key))));
2384 }
2385 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2386 auto locator = KJ_ASSERT_NONNULL(parentLocator.tryResolve(kj::str(entry.key, "/")));
2387 entries.add(js.alloc<FileSystemDirectoryHandle>(
2388 vfs, kj::mv(locator), jsg::USVString(kj::str(entry.key))));
2389 }
2390 KJ_CASE_ONEOF(_, workerd::FsError) {
2391 JSG_FAIL_REQUIRE(DOMOperationError, "Symbolic link recursion detected"_kj);
2392 }
2393 }
2394 }
2395 }
2396 }
2397 }
2398 return entries.releaseAsArray();
2399}
2400 
2401auto resolveDirectoryHandle(jsg::Lock& js, const VirtualFileSystem& vfs, const jsg::Url& locator) {
2402 auto pathname = locator.getPathname();
2403 if (pathname.endsWith("/"_kj)) {
2404 pathname = pathname.first(pathname.size() - 1);
2405 auto cloned = locator.clone();
2406 cloned.setPathname(pathname);
2407 return vfs.resolve(js, cloned, {});
2408 }
2409 // Otherwise fall-back to the original locator.
2410 return vfs.resolve(js, locator, {});
2411}
2412} // namespace
2413 
2414jsg::Ref<FileSystemDirectoryHandle::EntryIterator> FileSystemDirectoryHandle::entries(
2415 jsg::Lock& js) {
2416 KJ_IF_SOME(existing, resolveDirectoryHandle(js, getVfs(), getLocator())) {
2417 KJ_SWITCH_ONEOF(existing) {
2418 KJ_CASE_ONEOF(err, workerd::FsError) {
2419 JSG_FAIL_REQUIRE(DOMOperationError, "Failed to read directory: ", static_cast<int>(err));
2420 }
2421 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2422 return js.alloc<EntryIterator>(
2423 IteratorState(JSG_THIS, collectEntries(getVfs(), js, kj::mv(dir), getLocator())));
2424 }
2425 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2426 JSG_FAIL_REQUIRE(DOMTypeMismatchError, "Not a directory");
2427 }
2428 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2429 JSG_FAIL_REQUIRE(DOMTypeMismatchError, "Not a directory");
2430 }
2431 }
2432 KJ_UNREACHABLE;
2433 }
2434 
2435 // The directory was not found. However, for some weird reason the spec requires that
2436 // we still return an iterator here but it needs to throw a NotFoundError when next
2437 // is actually called.
2438 auto ex = js.domException(kj::str("NotFoundError"), kj::str("Not found"));
2439 auto handle = jsg::JsValue(KJ_ASSERT_NONNULL(ex.tryGetHandle(js)));
2440 return js.alloc<EntryIterator>(IteratorState(jsg::JsRef(js, handle)));
2441}
2442 
2443jsg::Ref<FileSystemDirectoryHandle::KeyIterator> FileSystemDirectoryHandle::keys(jsg::Lock& js) {
2444 KJ_IF_SOME(existing, resolveDirectoryHandle(js, getVfs(), getLocator())) {
2445 KJ_SWITCH_ONEOF(existing) {
2446 KJ_CASE_ONEOF(err, workerd::FsError) {
2447 JSG_FAIL_REQUIRE(DOMOperationError, "Failed to read directory: ", static_cast<int>(err));
2448 }
2449 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2450 return js.alloc<KeyIterator>(
2451 IteratorState(JSG_THIS, collectEntries(getVfs(), js, kj::mv(dir), getLocator())));
2452 }
2453 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2454 JSG_FAIL_REQUIRE(DOMTypeMismatchError, "Not a directory");
2455 }
2456 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2457 JSG_FAIL_REQUIRE(DOMTypeMismatchError, "Not a directory");
2458 }
2459 }
2460 KJ_UNREACHABLE;
2461 }
2462 
2463 // The directory was not found. However, for some weird reason the spec requires that
2464 // we still return an iterator here but it needs to throw a NotFoundError when next
2465 // is actually called.
2466 auto ex = js.domException(kj::str("NotFoundError"), kj::str("Not found"));
2467 auto handle = jsg::JsValue(KJ_ASSERT_NONNULL(ex.tryGetHandle(js)));
2468 return js.alloc<KeyIterator>(IteratorState(jsg::JsRef(js, handle)));
2469}
2470 
2471jsg::Ref<FileSystemDirectoryHandle::ValueIterator> FileSystemDirectoryHandle::values(
2472 jsg::Lock& js) {
2473 KJ_IF_SOME(existing, resolveDirectoryHandle(js, getVfs(), getLocator())) {
2474 KJ_SWITCH_ONEOF(existing) {
2475 KJ_CASE_ONEOF(err, workerd::FsError) {
2476 JSG_FAIL_REQUIRE(DOMOperationError, "Failed to read directory: ", static_cast<int>(err));
2477 }
2478 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2479 return js.alloc<ValueIterator>(
2480 IteratorState(JSG_THIS, collectEntries(getVfs(), js, kj::mv(dir), getLocator())));
2481 }
2482 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2483 JSG_FAIL_REQUIRE(DOMTypeMismatchError, "Not a directory");
2484 }
2485 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2486 JSG_FAIL_REQUIRE(DOMTypeMismatchError, "Not a directory");
2487 }
2488 }
2489 KJ_UNREACHABLE;
2490 }
2491 
2492 // The directory was not found. However, for some weird reason the spec requires that
2493 // we still return an iterator here but it needs to throw a NotFoundError when next
2494 // is actually called.
2495 auto ex = js.domException(kj::str("NotFoundError"), kj::str("Not found"));
2496 auto handle = jsg::JsValue(KJ_ASSERT_NONNULL(ex.tryGetHandle(js)));
2497 return js.alloc<ValueIterator>(IteratorState(jsg::JsRef(js, handle)));
2498}
2499 
2500void FileSystemDirectoryHandle::forEach(jsg::Lock& js,
2501 jsg::Function<void(
2502 jsg::USVString, jsg::Ref<FileSystemHandle>, jsg::Ref<FileSystemDirectoryHandle>)> callback,
2503 jsg::Optional<jsg::Value> thisArg,
2504 const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& exception) {
2505 
2506 KJ_IF_SOME(existing, resolveDirectoryHandle(js, getVfs(), getLocator())) {
2507 KJ_SWITCH_ONEOF(existing) {
2508 KJ_CASE_ONEOF(err, workerd::FsError) {
2509 js.throwException(js.v8Ref(exception.wrap(js, fsErrorToDomException(js, err))));
2510 }
2511 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2512 auto receiver = js.v8Undefined();
2513 KJ_IF_SOME(arg, thisArg) {
2514 auto handle = arg.getHandle(js);
2515 if (!handle->IsNullOrUndefined()) {
2516 receiver = handle;
2517 }
2518 }
2519 callback.setReceiver(js.v8Ref(receiver));
2520 
2521 for (auto& entry: collectEntries(getVfs(), js, kj::mv(dir), getLocator())) {
2522 callback(js, jsg::USVString(kj::str(entry->getName(js))), entry.addRef(), JSG_THIS);
2523 }
2524 return;
2525 }
2526 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2527 JSG_FAIL_REQUIRE(DOMTypeMismatchError, "Not a directory");
2528 }
2529 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2530 JSG_FAIL_REQUIRE(DOMTypeMismatchError, "Not a directory");
2531 }
2532 }
2533 KJ_UNREACHABLE;
2534 }
2535 
2536 JSG_FAIL_REQUIRE(DOMNotFoundError, "Not found");
2537}
2538 
2539FileSystemFileHandle::FileSystemFileHandle(
2540 const workerd::VirtualFileSystem& vfs, jsg::Url locator, jsg::USVString name)
2541 : FileSystemHandle(vfs, kj::mv(locator), kj::mv(name)) {}
2542 
2543jsg::Promise<jsg::Ref<File>> FileSystemFileHandle::getFile(
2544 jsg::Lock& js, const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& deHandler) {
2545 // TODO(node-fs): Currently this copies the file data into the new File object.
2546 // Alternatively, File/Blob can be modified to allow it to be backed by a
2547 // workerd::File such that it does not need to create a separate in-memory
2548 // copy of the data. We can make that optimization as a follow-up, however.
2549 
2550 // First, let's use the locator and vfs to see if the file actually exists.
2551 KJ_IF_SOME(item, getVfs().resolve(js, getLocator(), {})) {
2552 KJ_SWITCH_ONEOF(item) {
2553 KJ_CASE_ONEOF(err, workerd::FsError) {
2554 return js.rejectedPromise<jsg::Ref<File>>(
2555 deHandler.wrap(js, fsErrorToDomException(js, err)));
2556 }
2557 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2558 auto stat = file->stat(js);
2559 KJ_SWITCH_ONEOF(file->readAllBytes(js)) {
2560 KJ_CASE_ONEOF(bytes, jsg::BufferSource) {
2561 return js.resolvedPromise(
2562 js.alloc<File>(js, bytes.getJsHandle(js), jsg::USVString(kj::str(getName(js))),
2563 kj::String(), (stat.lastModified - kj::UNIX_EPOCH) / kj::MILLISECONDS));
2564 }
2565 KJ_CASE_ONEOF(err, workerd::FsError) {
2566 return js.rejectedPromise<jsg::Ref<File>>(
2567 deHandler.wrap(js, fsErrorToDomException(js, err)));
2568 }
2569 }
2570 KJ_UNREACHABLE;
2571 }
2572 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2573 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Is a directory"));
2574 return js.rejectedPromise<jsg::Ref<File>>(deHandler.wrap(js, kj::mv(ex)));
2575 }
2576 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2577 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Is a symbolic link"));
2578 return js.rejectedPromise<jsg::Ref<File>>(deHandler.wrap(js, kj::mv(ex)));
2579 }
2580 }
2581 KJ_UNREACHABLE;
2582 }
2583 
2584 // If the file does not exist, we reject the promise with a NotFoundError.
2585 auto ex = js.domException(kj::str("NotFoundError"), kj::str("Not found"));
2586 return js.rejectedPromise<jsg::Ref<File>>(deHandler.wrap(js, kj::mv(ex)));
2587}
2588 
2589jsg::Promise<jsg::Ref<FileSystemWritableFileStream>> FileSystemFileHandle::createWritable(
2590 jsg::Lock& js,
2591 jsg::Optional<FileSystemCreateWritableOptions> options,
2592 const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& deHandler,
2593 const jsg::TypeHandler<FileSystemWritableData>& dataHandler) {
2594 
2595 // Per the spec, the writable stream we create here is expected to write into
2596 // a temporary space until the stream is closed. When closed, the original file
2597 // contents are replaced with the new contents. If the stream is aborted or
2598 // errored, the temporary file data is discarded.
2599 auto opts = options.orDefault(FileSystemCreateWritableOptions{});
2600 
2601 // If keepExistingData is true, the temporary file is created with a copy of
2602 // the original file data. Otherwise, the temporary file is created empty,
2603 // which means that if we create a writable stream and close it without writing
2604 // anything, the original file data is lost.
2605 bool keepExistingData = opts.keepExistingData.orDefault(false);
2606 
2607 kj::Maybe<kj::Rc<workerd::File>> fileData;
2608 KJ_IF_SOME(existing, getVfs().resolve(js, getLocator(), {})) {
2609 if (keepExistingData) {
2610 KJ_SWITCH_ONEOF(existing) {
2611 KJ_CASE_ONEOF(err, workerd::FsError) {
2612 return js.rejectedPromise<jsg::Ref<FileSystemWritableFileStream>>(
2613 deHandler.wrap(js, fsErrorToDomException(js, err)));
2614 }
2615 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2616 KJ_SWITCH_ONEOF(file->clone(js)) {
2617 KJ_CASE_ONEOF(err, workerd::FsError) {
2618 return js.rejectedPromise<jsg::Ref<FileSystemWritableFileStream>>(
2619 deHandler.wrap(js, fsErrorToDomException(js, err)));
2620 }
2621 KJ_CASE_ONEOF(cloned, kj::Rc<workerd::File>) {
2622 fileData = kj::mv(cloned);
2623 }
2624 }
2625 }
2626 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2627 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Is a directory"));
2628 return js.rejectedPromise<jsg::Ref<FileSystemWritableFileStream>>(
2629 deHandler.wrap(js, kj::mv(ex)));
2630 }
2631 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2632 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Is a symbolic link"));
2633 return js.rejectedPromise<jsg::Ref<FileSystemWritableFileStream>>(
2634 deHandler.wrap(js, kj::mv(ex)));
2635 }
2636 }
2637 } else {
2638 fileData = workerd::File::newWritable(js);
2639 }
2640 } else {
2641 auto ex = js.domException(kj::str("NotFoundError"), kj::str("File not found"));
2642 return js.rejectedPromise<jsg::Ref<FileSystemWritableFileStream>>(
2643 deHandler.wrap(js, kj::mv(ex)));
2644 }
2645 
2646 auto sharedState = kj::rc<FileSystemWritableFileStream::State>(
2647 js, getVfs(), JSG_THIS, KJ_ASSERT_NONNULL(kj::mv(fileData)));
2648 auto stream =
2649 js.alloc<FileSystemWritableFileStream>(newWritableStreamJsController(), sharedState.addRef());
2650 
2651 UnderlyingSink sink;
2652 // Per the WHATWG spec, the type property for WritableStream's underlying sink must be undefined.
2653 // The "bytes" type is only valid for ReadableStream. When pedantic_wpt is not set, we preserve
2654 // the legacy behavior of setting the type to "bytes".
2655 if (!FeatureFlags::get(js).getPedanticWpt()) {
2656 sink.type = kj::str("bytes");
2657 }
2658 sink.write = [state = sharedState.addRef(), &deHandler, &dataHandler](
2659 jsg::Lock& js, v8::Local<v8::Value> chunk, auto c) mutable {
2660 return js.tryCatch([&] {
2661 KJ_IF_SOME(unwrapped, dataHandler.tryUnwrap(js, chunk)) {
2662 return FileSystemWritableFileStream::writeImpl(
2663 js, kj::mv(unwrapped), *state.get(), deHandler);
2664 }
2665 return js.rejectedPromise<void>(
2666 js.typeError("WritableStream received a value that is not writable"));
2667 }, [&](jsg::Value exception) { return js.rejectedPromise<void>(kj::mv(exception)); });
2668 };
2669 sink.abort = [state = sharedState.addRef()](jsg::Lock& js, auto reason) mutable {
2670 // When aborted, we just drop any of the written data on the floor.
2671 state->clear();
2672 return js.resolvedPromise();
2673 };
2674 sink.close = [state = sharedState.addRef(), &deHandler](jsg::Lock& js) mutable {
2675 KJ_DEFER(state->clear());
2676 return js.tryCatch([&] {
2677 KJ_IF_SOME(temp, state->temp) {
2678 auto basePath = kj::str(state->file->getLocator().getPathname().slice(1));
2679 kj::Path root{};
2680 auto base = root.eval(basePath);
2681 
2682 KJ_IF_SOME(existing,
2683 state->vfs.getRoot(js)->tryOpen(js, base,
2684 {
2685 .createAs = workerd::FsType::FILE,
2686 })) {
2687 KJ_SWITCH_ONEOF(existing) {
2688 KJ_CASE_ONEOF(err, workerd::FsError) {
2689 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2690 }
2691 KJ_CASE_ONEOF(dir, kj::Rc<workerd::Directory>) {
2692 auto ex = js.domException(kj::str("TypeMismatchError"), kj::str("Is a directory"));
2693 return js.rejectedPromise<void>(deHandler.wrap(js, kj::mv(ex)));
2694 }
2695 KJ_CASE_ONEOF(file, kj::Rc<workerd::File>) {
2696 KJ_IF_SOME(err, file->replace(js, temp.addRef())) {
2697 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2698 }
2699 return js.resolvedPromise();
2700 }
2701 KJ_CASE_ONEOF(link, kj::Rc<workerd::SymbolicLink>) {
2702 auto ex =
2703 js.domException(kj::str("TypeMismatchError"), kj::str("Is a symbolic link"));
2704 return js.rejectedPromise<void>(deHandler.wrap(js, kj::mv(ex)));
2705 }
2706 }
2707 KJ_UNREACHABLE;
2708 }
2709 auto ex =
2710 js.domException(kj::str("InvalidStateError"), kj::str("Failed to open or create file"));
2711 return js.rejectedPromise<void>(deHandler.wrap(js, kj::mv(ex)));
2712 }
2713 return js.resolvedPromise();
2714 }, [&](jsg::Value exception) { return js.rejectedPromise<void>(kj::mv(exception)); });
2715 };
2716 stream->getController().setup(js, kj::mv(sink), kj::none);
2717 
2718 return js.resolvedPromise(kj::mv(stream));
2719}
2720 
2721FileSystemWritableFileStream::FileSystemWritableFileStream(
2722 kj::Own<WritableStreamController> controller, kj::Rc<State> sharedState)
2723 : WritableStream(kj::mv(controller)),
2724 sharedState(kj::mv(sharedState)) {}
2725 
2726jsg::Promise<void> FileSystemWritableFileStream::write(jsg::Lock& js,
2727 kj::OneOf<jsg::Ref<Blob>, jsg::BufferSource, kj::String, WriteParams> data,
2728 const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& deHandler) {
2729 JSG_REQUIRE(!getController().isLockedToWriter(), TypeError,
2730 "Cannot write to a stream that is locked to a reader");
2731 auto writer = getWriter(js);
2732 KJ_DEFER(writer->releaseLock(js));
2733 return writeImpl(js, kj::mv(data), *sharedState.get(), deHandler);
2734}
2735 
2736jsg::Promise<void> FileSystemWritableFileStream::writeImpl(jsg::Lock& js,
2737 FileSystemWritableData data,
2738 State& state,
2739 const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& deHandler) {
2740 KJ_IF_SOME(inner, state.temp) {
2741 return js.tryCatch([&] {
2742 KJ_SWITCH_ONEOF(data) {
2743 KJ_CASE_ONEOF(blob, jsg::Ref<Blob>) {
2744 KJ_SWITCH_ONEOF(inner->write(js, state.position, blob->getData())) {
2745 KJ_CASE_ONEOF(written, uint32_t) {
2746 state.position += written;
2747 }
2748 KJ_CASE_ONEOF(err, workerd::FsError) {
2749 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2750 }
2751 }
2752 }
2753 KJ_CASE_ONEOF(buffer, jsg::BufferSource) {
2754 KJ_SWITCH_ONEOF(inner->write(js, state.position, buffer)) {
2755 KJ_CASE_ONEOF(written, uint32_t) {
2756 state.position += written;
2757 }
2758 KJ_CASE_ONEOF(err, workerd::FsError) {
2759 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2760 }
2761 }
2762 }
2763 KJ_CASE_ONEOF(str, kj::String) {
2764 KJ_SWITCH_ONEOF(inner->write(js, state.position, str)) {
2765 KJ_CASE_ONEOF(written, uint32_t) {
2766 state.position += written;
2767 }
2768 KJ_CASE_ONEOF(err, workerd::FsError) {
2769 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2770 }
2771 }
2772 }
2773 KJ_CASE_ONEOF(params, WriteParams) {
2774 uint32_t offset = state.position;
2775 KJ_IF_SOME(pos, params.position) {
2776 auto stat = inner->stat(js);
2777 if (pos > stat.size) {
2778 KJ_IF_SOME(err, inner->resize(js, offset)) {
2779 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2780 }
2781 }
2782 offset = pos;
2783 }
2784 
2785 if (params.type == "write"_kj) {
2786 KJ_IF_SOME(maybeData, params.data) {
2787 KJ_IF_SOME(data, maybeData) {
2788 KJ_SWITCH_ONEOF(data) {
2789 KJ_CASE_ONEOF(blob, jsg::Ref<Blob>) {
2790 KJ_SWITCH_ONEOF(inner->write(js, offset, blob->getData())) {
2791 KJ_CASE_ONEOF(written, uint32_t) {
2792 state.position = offset + written;
2793 return js.resolvedPromise();
2794 }
2795 KJ_CASE_ONEOF(err, workerd::FsError) {
2796 return js.rejectedPromise<void>(
2797 deHandler.wrap(js, fsErrorToDomException(js, err)));
2798 }
2799 }
2800 KJ_UNREACHABLE;
2801 }
2802 KJ_CASE_ONEOF(buffer, jsg::BufferSource) {
2803 KJ_SWITCH_ONEOF(inner->write(js, offset, buffer)) {
2804 KJ_CASE_ONEOF(written, uint32_t) {
2805 state.position = offset + written;
2806 return js.resolvedPromise();
2807 }
2808 KJ_CASE_ONEOF(err, workerd::FsError) {
2809 return js.rejectedPromise<void>(
2810 deHandler.wrap(js, fsErrorToDomException(js, err)));
2811 }
2812 }
2813 KJ_UNREACHABLE;
2814 }
2815 KJ_CASE_ONEOF(str, kj::String) {
2816 KJ_SWITCH_ONEOF(inner->write(js, offset, str)) {
2817 KJ_CASE_ONEOF(written, uint32_t) {
2818 state.position = offset + written;
2819 return js.resolvedPromise();
2820 }
2821 KJ_CASE_ONEOF(err, workerd::FsError) {
2822 return js.rejectedPromise<void>(
2823 deHandler.wrap(js, fsErrorToDomException(js, err)));
2824 }
2825 }
2826 }
2827 KJ_UNREACHABLE;
2828 }
2829 } else {
2830 return js.rejectedPromise<void>(
2831 js.typeError("write() requires a non-null data parameter"));
2832 }
2833 }
2834 
2835 return js.rejectedPromise<void>(deHandler.wrap(js,
2836 js.domException(kj::str("SyntaxError"),
2837 kj::str("write() requires a non-null data parameter"))));
2838 
2839 } else if (params.type == "seek"_kj) {
2840 uint32_t pos;
2841 KJ_IF_SOME(s, params.position) {
2842 pos = s;
2843 } else {
2844 return js.rejectedPromise<void>(deHandler.wrap(js,
2845 js.domException(
2846 kj::str("SyntaxError"), kj::str("seek() requires a position parameter"))));
2847 }
2848 state.position = pos;
2849 auto stat = inner->stat(js);
2850 if (state.position > stat.size) {
2851 KJ_IF_SOME(err, inner->resize(js, state.position)) {
2852 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2853 }
2854 }
2855 } else if (params.type == "truncate"_kj) {
2856 uint32_t size = 0;
2857 KJ_IF_SOME(s, params.size) {
2858 size = s;
2859 } else {
2860 return js.rejectedPromise<void>(deHandler.wrap(js,
2861 js.domException(
2862 kj::str("SyntaxError"), kj::str("truncate() requires a size parameter"))));
2863 }
2864 KJ_IF_SOME(err, inner->resize(js, size)) {
2865 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2866 }
2867 auto stat = inner->stat(js);
2868 if (state.position > stat.size) {
2869 state.position = stat.size;
2870 }
2871 } else {
2872 return js.rejectedPromise<void>(
2873 js.typeError(kj::str("Invalid write type: ", params.type)));
2874 }
2875 }
2876 }
2877 
2878 return js.resolvedPromise();
2879 }, [&](jsg::Value exception) { return js.rejectedPromise<void>(kj::mv(exception)); });
2880 }
2881 
2882 return js.rejectedPromise<void>(js.typeError("write() after closed"));
2883}
2884 
2885jsg::Promise<void> FileSystemWritableFileStream::seek(jsg::Lock& js,
2886 uint32_t position,
2887 const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& deHandler) {
2888 KJ_IF_SOME(inner, sharedState->temp) {
2889 auto stat = inner->stat(js);
2890 if (position > stat.size) {
2891 KJ_IF_SOME(err, inner->resize(js, position)) {
2892 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2893 }
2894 }
2895 sharedState->position = position;
2896 return js.resolvedPromise();
2897 }
2898 
2899 return js.rejectedPromise<void>(js.typeError("seek() after closed"));
2900}
2901 
2902jsg::Promise<void> FileSystemWritableFileStream::truncate(
2903 jsg::Lock& js, uint32_t size, const jsg::TypeHandler<jsg::Ref<jsg::DOMException>>& deHandler) {
2904 KJ_IF_SOME(inner, sharedState->temp) {
2905 KJ_IF_SOME(err, inner->resize(js, size)) {
2906 return js.rejectedPromise<void>(deHandler.wrap(js, fsErrorToDomException(js, err)));
2907 }
2908 auto stat = inner->stat(js);
2909 if (sharedState->position > stat.size) {
2910 sharedState->position = stat.size;
2911 }
2912 return js.resolvedPromise();
2913 }
2914 
2915 return js.rejectedPromise<void>(js.typeError("seek() after closed"));
2916}
2917} // namespace workerd::api