diff --git a/src/docs/src/examples.js b/src/docs/src/examples.js index 6880b013a..f2b1bec70 100644 --- a/src/docs/src/examples.js +++ b/src/docs/src/examples.js @@ -420,6 +420,30 @@ const examples = [ slug: 'fs-delete-directory', source: '/playground/examples/fs-delete-directory.html', }, + { + title: 'Read a directory page by page', + description: 'Read a directory one page at a time with a cursor using Puter.js filesystem API. Run and experiment with this example in the playground.', + slug: 'fs-large-directory-page', + source: '/playground/examples/fs-large-directory-page.html', + }, + { + title: 'Stream directory pages', + description: 'Read a large directory with an async iterator using Puter.js filesystem API. Run and modify this example instantly in your browser.', + slug: 'fs-large-directory-stream', + source: '/playground/examples/fs-large-directory-stream.html', + }, + { + title: 'Sort a directory and include subdirectories', + description: 'Sort a directory listing and include subdirectories with Puter.js filesystem API. Run and experiment with this example in the playground.', + slug: 'fs-large-directory-sort', + source: '/playground/examples/fs-large-directory-sort.html', + }, + { + title: 'Count a directory while paging', + description: 'Get a total entry count alongside a page of results with Puter.js filesystem API. Run and modify this example directly in your browser.', + slug: 'fs-large-directory-total', + source: '/playground/examples/fs-large-directory-total.html', + }, { title: 'Share a file', description: 'Share a file with another user using Puter.js filesystem API. Run and experiment with this sharing example in the playground.', @@ -456,42 +480,6 @@ const examples = [ slug: 'fs-share-a-file-mode', source: '/playground/examples/fs-share-a-file-mode.html', }, -{ - title: 'page through a large directory', - description: 'Read a directory one page at a time with a cursor using Puter.js filesystem API. Run and experiment with this example in the playground.', - slug: 'fs-large-directory-page', - source: '/playground/examples/fs-large-directory-page.html', - }, -{ - title: 'stream directory pages', - description: 'Walk a large directory with an async iterator using Puter.js filesystem API. Run and modify this example instantly in your browser.', - slug: 'fs-large-directory-stream', - source: '/playground/examples/fs-large-directory-stream.html', - }, -{ - title: 'sort and descend a directory', - description: 'Sort a directory listing and read subdirectories with Puter.js filesystem API. Run and experiment with this example in the playground.', - slug: 'fs-large-directory-sort', - source: '/playground/examples/fs-large-directory-sort.html', - }, -{ - title: 'count a directory while paging', - description: 'Get a total entry count alongside a page of results with Puter.js filesystem API. Run and modify this example directly in your browser.', - slug: 'fs-large-directory-total', - source: '/playground/examples/fs-large-directory-total.html', - }, -{ - title: 'write without clobbering', - description: 'Keep both files or refuse to replace one with dedupeName and overwrite in Puter.js filesystem API. Run and experiment with this example in the playground.', - slug: 'fs-large-directory-write', - source: '/playground/examples/fs-large-directory-write.html', - }, -{ - title: 'create the folders on the way', - description: 'Write into a directory tree that does not exist yet with Puter.js filesystem API. Run and modify this example instantly in your browser.', - slug: 'fs-large-directory-parents', - source: '/playground/examples/fs-large-directory-parents.html', - }, ], }, { diff --git a/src/docs/src/playground/examples/fs-large-directory-page.html b/src/docs/src/playground/examples/fs-large-directory-page.html index b8c090a0d..3d1a98672 100644 --- a/src/docs/src/playground/examples/fs-large-directory-page.html +++ b/src/docs/src/playground/examples/fs-large-directory-page.html @@ -1,35 +1,41 @@ -

File browser

- + +
diff --git a/src/docs/src/playground/examples/fs-large-directory-parents.html b/src/docs/src/playground/examples/fs-large-directory-parents.html deleted file mode 100644 index d417cdba9..000000000 --- a/src/docs/src/playground/examples/fs-large-directory-parents.html +++ /dev/null @@ -1,26 +0,0 @@ - - - -

Report generator

- - - diff --git a/src/docs/src/playground/examples/fs-large-directory-write.html b/src/docs/src/playground/examples/fs-large-directory-write.html deleted file mode 100644 index e4cbddd56..000000000 --- a/src/docs/src/playground/examples/fs-large-directory-write.html +++ /dev/null @@ -1,31 +0,0 @@ - - - -

Upload endpoint

- - - - - diff --git a/src/docs/src/recipes/store-files.md b/src/docs/src/recipes/store-files.md index 6a646ff5f..5e6d7d544 100644 --- a/src/docs/src/recipes/store-files.md +++ b/src/docs/src/recipes/store-files.md @@ -23,13 +23,21 @@ await puter.fs.write('notes/todo.txt', 'Buy milk'); The contents can be a `String`, `File`, `Blob`, `ArrayBuffer` or typed array, so text and binary data are written the same way. Writing to a path that already -exists replaces the file, and passing `dedupeName` keeps both copies by saving -the new one under a free name: +exists replaces the file. Passing `dedupeName` keeps both copies by saving the +new one under a free name, and passing `overwrite: false` rejects the write +instead: ```js +// Saves photo-1.png if photo.png is taken. await puter.fs.write('uploads/photo.png', blob, { dedupeName: true }); + +// Rejects if photo.png is taken. +await puter.fs.write('uploads/photo.png', blob, { overwrite: false }); ``` +Use `dedupeName` when two users might upload a file with the same name and both +files should be kept. + Writing into a directory that does not exist yet is one more option: ```js @@ -60,6 +68,20 @@ A relative path resolves against `~/AppData//`, the sandbox Puter creates for your app the first time the user signs in. You can create any files and folders you like inside it, and your app cannot see anything outside it. +## Create a Folder + +To create a directory, use the [`puter.fs.mkdir()`](/FS/mkdir/) method. With +`createMissingParents`, it creates a whole tree in one call: + +```js +await puter.fs.mkdir('reports/2026/q3', { createMissingParents: true }); +``` + +The [`mkdir()`](/FS/mkdir/) method takes the same `dedupeName` and `overwrite` +options as [`write()`](/FS/write/). The difference is the default. Creating a +directory that already exists fails unless you pass `overwrite: true`, while a +write replaces an existing file. + ## Upload To take a file from an ``, use the @@ -87,8 +109,9 @@ const items = await puter.fs.readdir('uploads', { sortBy: 'modified', sortOrder: ``` It pages the way a key listing does, with `limit` and `cursor`, or `stream: -true` for `for await`, which is what keeps a directory of thousands affordable -to display. +true` for `for await`. [Work with a large +directory](/recipes/work-with-a-large-directory/) covers paging, sorting and +counting a directory of thousands of files. To read the metadata of one file, use the [`puter.fs.stat()`](/FS/stat/) method: diff --git a/src/docs/src/recipes/work-with-a-large-directory.md b/src/docs/src/recipes/work-with-a-large-directory.md index 6302f8c11..224fbfa8a 100644 --- a/src/docs/src/recipes/work-with-a-large-directory.md +++ b/src/docs/src/recipes/work-with-a-large-directory.md @@ -1,31 +1,23 @@ --- title: Work With a Large Directory -description: "Page through a directory that is too big to read at once, sort and descend into it, and write into it without clobbering what is already there." +description: "Learn how to list a directory with thousands of files in Puter.js, so your app can load and show it one page at a time." tags: [fs, performance] order: 46 --- -**Use this when** a directory has more in it than you want in memory at once, or -when you are writing into a folder that other things are also writing into. -Typically: +Reading a directory with [`puter.fs.readdir()`](/FS/readdir/) returns every item +in one call, as shown in [Store Files](/recipes/store-files/). That works for a +folder of a few dozen files. A folder of user uploads or generated exports can +grow to thousands of items, and loading all of them before showing anything +makes your app slow. The same method can return the directory in pages, sort +it, include subdirectories and count it, which is what a file browser with +infinite scrolling and a "showing 50 of 214" label needs. -- A **file browser** with an infinite-scrolling list, a sort header, and a - "showing 50 of 214" label. -- A **media gallery** that shows the newest uploads first and walks - subdirectories to find them. -- An **upload endpoint** where two people can send `invoice.pdf` a second apart - and neither should lose theirs. -- A **report generator** writing to `reports/2026/q3/` before that folder tree - exists. +## Read One Page at a Time -Reading a whole directory is one call and covered in -[storing files](/recipes/store-files/). This is what to do when that is no -longer enough. - -## Page through it - -`readdir()` normally resolves to a plain array. Passing `cursor` — even `null` -for the first page — switches it to a page object instead: +By default, the [`readdir()`](/FS/readdir/) method resolves to a plain array. +Passing a `cursor` switches the result to a page object with `items` and +`cursor`. For the first page, pass `null`: ```js let page = await puter.fs.readdir({ path: 'uploads', cursor: null, limit: 100 }); @@ -39,13 +31,17 @@ for (;;) { } ``` -Stop when `cursor` is absent. It is present only while more pages remain, so -that is the condition — not comparing `items.length` to `limit`. +Each page includes a `cursor` if there are more pages to load. When `cursor` is +missing, you've reached the last page. -## Stream the pages instead +Paging with a cursor is fast on every page. The `offset` option also works, but +the server has to count past every item you skip, so later pages get slower. -`stream: true` hands back an async iterator, which is usually what you want when -you are just walking the whole thing: +## Stream the Pages + +To read the whole directory, pass `stream: true`. The +[`readdir()`](/FS/readdir/) method then returns an async iterator that you read +with `for await`: ```js for await (const page of puter.fs.readdir({ path: 'uploads', stream: true })) { @@ -55,9 +51,12 @@ for await (const page of puter.fs.readdir({ path: 'uploads', stream: true })) { } ``` -Same pages, less bookkeeping. Combine it with `limit` to set the page size. +Each loop gives you one page, and the cursor is handled for you. Set +`limit` to choose the page size. Streaming cannot be combined with `offset`. -## Sort it, and descend +## Sort the Results + +To change the order, pass `sortBy` and `sortOrder`: ```js const items = await puter.fs.readdir({ @@ -67,10 +66,14 @@ const items = await puter.fs.readdir({ }); ``` -`sortBy` takes `name` (the default), `modified`, `type` or `size`. +The `sortBy` option takes `name` (the default), `modified`, `type` or `size`, +and `sortOrder` takes `asc` (the default) or `desc`. When you read pages with a +cursor, every page must use the same sort as the first one. -`recursive: true` lists what is in the subdirectories too, and `depth` bounds -how far down it goes: +## Include Subdirectories + +To list the contents of subdirectories as well, pass `recursive: true`. Set +`depth` to limit how many levels down it goes: ```js const items = await puter.fs.readdir({ @@ -80,62 +83,23 @@ const items = await puter.fs.readdir({ }); ``` -Sorting interacts with recursion in a way worth knowing: sorting by `name` -orders by *full path*, so each directory's contents stay together. The other -fields sort across the whole subtree, so files from different folders interleave. +With `recursive`, sorting by `name` sorts by full path, so the contents of each +directory stay together. Sorting by `modified`, `type` or `size` sorts across +all levels at once, so files from different directories are mixed together. -## Count it +## Count the Items + +To show a total such as "showing 50 of 214", pass `includeTotal: true`. It also +switches the result to a page object, so you get the count along with the first +page: ```js -const { items, total } = await puter.fs.readdir({ +const { items, total, cursor } = await puter.fs.readdir({ path: 'uploads', cursor: null, + limit: 50, includeTotal: true, }); ``` -`includeTotal` also switches the result to a page object, so you get a count -without giving up paging. - -## Write into it without clobbering - -`write()` overwrites by default. Two options change that: - -```js -// Keep both: writes report-1.txt if report.txt is taken. -await puter.fs.write('uploads/report.txt', data, { dedupeName: true }); - -// Refuse instead of replacing. -await puter.fs.write('uploads/report.txt', data, { overwrite: false }); -``` - -`dedupeName` is what you want when two users might upload the same filename and -you would rather keep both than lose one. - -## Create the folders on the way - -Writing into a folder that does not exist fails unless you say otherwise: - -```js -await puter.fs.write('reports/2026/q3/summary.txt', data, { - createMissingParents: true, -}); -``` - -`mkdir()` takes the same three options, so a directory tree can be created in -one call rather than level by level. - -## Notes - -- `offset` still works but gets slower the further in you go, because the server - counts past everything you skipped. Prefer `cursor`. -- **A cursor pins the sort.** Later pages must not ask for a different `sortBy` - or `sortOrder`, and `stream` cannot be combined with `offset`. -- Each item carries `is_shared`. Only shares on the item itself count — the - children of a folder you shared report `false`, because the share lives on the - folder. See [sharing a file](/recipes/share-a-file/). -- `mkdir()` defaults `overwrite` to `false`, while `write()` defaults it to - `true`. They are not the same, which is easy to assume. -- The same three result shapes — array, page object, async iterator — are how - the other listing methods work too, including - [`puter.fs.listShared()`](/FS/listShared/). +When you stream with `includeTotal`, only the first page carries `total`.