docs: add the medium fs recipe

readdir paging, streaming, sorting and recursion, plus the write and
mkdir options that decide what happens to an existing name. One
playground example per section.
This commit is contained in:
Juan Castro authored and GitHub committed 2026-10-05 21:36:53 +07:00
1 parent 445588f857
commit 1f2a42d55e
8 files changed
+322

No files matched your search

+36
View File
@@ -456,6 +456,42 @@ 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',
},
],
},
{
@@ -0,0 +1,24 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
await puter.fs.mkdir('uploads', { createMissingParents: true }).catch(() => {});
for (let i = 0; i < 7; i++) {
await puter.fs.write(`uploads/file-${i}.txt`, `n=${i}`);
}
// cursor (even null) switches the result to a page object.
let page = await puter.fs.readdir({ path: 'uploads', cursor: null, limit: 3 });
let n = 0;
for (;;) {
puter.print(`<b>page ${++n}</b>: ${page.items.map(i => i.name).join(', ')}<br>`);
// Present only while more pages remain — that is the stop condition.
if (!page.cursor) break;
page = await puter.fs.readdir({ path: 'uploads', cursor: page.cursor, limit: 3 });
}
})();
</script>
</body>
</html>
@@ -0,0 +1,25 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
// Without createMissingParents this fails: reports/2026/q3 is absent.
try {
await puter.fs.write('reports/2026/q3/summary.txt', 'no parents');
puter.print('wrote it (the folders already existed)<br>');
} catch (e) {
puter.print(`without the option: ${e.code ?? e.message}<br>`);
}
await puter.fs.write('reports/2026/q3/summary.txt', 'with parents', {
createMissingParents: true,
});
puter.print('with createMissingParents: written<br><br>');
// mkdir takes the same three options.
await puter.fs.mkdir('reports/2027/q1', { createMissingParents: true });
puter.print('and the same for mkdir');
})();
</script>
</body>
</html>
@@ -0,0 +1,36 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<button id="name">Sort by name</button>
<button id="modified">Newest first</button>
<button id="deep">Recursive, depth 2</button>
<script>
const setup = async () => {
await puter.fs.write('uploads/a.txt', 'a', { createMissingParents: true });
await puter.fs.write('uploads/b.txt', 'b');
await puter.fs.write('uploads/nested/c.txt', 'c', { createMissingParents: true });
};
const show = (items) =>
puter.print(items.map(i => i.name).join(', ') + '<br>');
document.getElementById('name').addEventListener('click', async () => {
await setup();
show(await puter.fs.readdir({ path: 'uploads', sortBy: 'name' }));
});
document.getElementById('modified').addEventListener('click', async () => {
await setup();
show(await puter.fs.readdir({
path: 'uploads', sortBy: 'modified', sortOrder: 'desc',
}));
});
document.getElementById('deep').addEventListener('click', async () => {
await setup();
// Sorting by name orders by full path, so folders stay together.
show(await puter.fs.readdir({ path: 'uploads', recursive: true, depth: 2 }));
});
</script>
</body>
</html>
@@ -0,0 +1,19 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
await puter.fs.mkdir('uploads', { createMissingParents: true }).catch(() => {});
for (let i = 0; i < 7; i++) {
await puter.fs.write(`uploads/file-${i}.txt`, `n=${i}`);
}
// Same pages as the cursor loop, without the bookkeeping.
let n = 0;
for await (const page of puter.fs.readdir({ path: 'uploads', stream: true, limit: 3 })) {
puter.print(`<b>page ${++n}</b>: ${page.items.map(i => i.name).join(', ')}<br>`);
}
})();
</script>
</body>
</html>
@@ -0,0 +1,22 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
await puter.fs.write('uploads/a.txt', 'a', { createMissingParents: true });
await puter.fs.write('uploads/b.txt', 'b');
// includeTotal also switches the result to a page object.
const { items, total } = await puter.fs.readdir({
path: 'uploads',
cursor: null,
limit: 1,
includeTotal: true,
});
puter.print(`this page: ${items.length}<br>`);
puter.print(`total in the directory: ${total}`);
})();
</script>
</body>
</html>
@@ -0,0 +1,29 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<button id="dedupe">Write with dedupeName</button>
<button id="refuse">Write with overwrite: false</button>
<script>
document.getElementById('dedupe').addEventListener('click', async () => {
// Keeps both rather than replacing: report-1.txt, report-2.txt, …
const file = await puter.fs.write('uploads/report.txt', 'data', {
dedupeName: true,
createMissingParents: true,
});
puter.print(`wrote ${file.name}<br>`);
});
document.getElementById('refuse').addEventListener('click', async () => {
try {
await puter.fs.write('uploads/report.txt', 'data', {
overwrite: false,
createMissingParents: true,
});
puter.print('wrote it — the name was free<br>');
} catch (e) {
puter.print(`refused: ${e.code ?? e.message}<br>`);
}
});
</script>
</body>
</html>
@@ -0,0 +1,131 @@
---
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."
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.
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:
```js
let page = await puter.fs.readdir({ path: 'uploads', cursor: null, limit: 100 });
for (;;) {
for (const item of page.items) {
console.log(item.name);
}
if (!page.cursor) break;
page = await puter.fs.readdir({ path: 'uploads', cursor: page.cursor, limit: 100 });
}
```
Stop when `cursor` is absent. It is present only while more pages remain, so
that is the condition — not comparing `items.length` to `limit`.
## Stream the pages instead
`stream: true` hands back an async iterator, which is usually what you want when
you are just walking the whole thing:
```js
for await (const page of puter.fs.readdir({ path: 'uploads', stream: true })) {
for (const item of page.items) {
console.log(item.name);
}
}
```
Same pages, less bookkeeping. Combine it with `limit` to set the page size.
## Sort it, and descend
```js
const items = await puter.fs.readdir({
path: 'uploads',
sortBy: 'modified',
sortOrder: 'desc',
});
```
`sortBy` takes `name` (the default), `modified`, `type` or `size`.
`recursive: true` lists what is in the subdirectories too, and `depth` bounds
how far down it goes:
```js
const items = await puter.fs.readdir({
path: 'uploads',
recursive: true,
depth: 2,
});
```
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.
## Count it
```js
const { items, total } = await puter.fs.readdir({
path: 'uploads',
cursor: null,
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/).