This commit is contained in:
Reynaldi Chernando authored and GitHub committed 2026-10-05 21:36:53 +07:00
1 parent b52128b46a
commit cdc40aeee6
6 files changed
+124 -200

No files matched your search

+24 -36
View File
@@ -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',
},
],
},
{
@@ -1,35 +1,41 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<h3>File browser</h3>
<button id="more">Load more</button>
<button id="next" disabled>Load next page</button>
<div id="output"></div>
<script>
// An infinite-scrolling list: one page per scroll to the bottom,
// standing in here for a button.
let cursor = null, done = false, seeded = false;
const button = document.getElementById('next');
const output = document.getElementById('output');
let cursor = null;
let pageNumber = 0;
document.getElementById('more').addEventListener('click', async () => {
if (done) return;
// Read one page of 3 items and pass the cursor to get the next one
async function loadNextPage() {
const page = await puter.fs.readdir({ path: 'reports', cursor, limit: 3 });
pageNumber++;
output.innerHTML += `Page ${pageNumber}: ${page.items.map(item => item.name).join(', ')}<br>`;
if (!seeded) {
for (let i = 0; i < 7; i++) {
await puter.fs.write(`uploads/file-${i}.txt`, `n=${i}`, {
createMissingParents: true,
});
}
seeded = true;
// A missing cursor means this was the last page
if (page.cursor) {
cursor = page.cursor;
button.disabled = false;
} else {
output.innerHTML += 'No more pages<br>';
button.disabled = true;
}
}
const page = await puter.fs.readdir({ path: 'uploads', cursor, limit: 3 });
for (const item of page.items) puter.print(`${item.name}<br>`);
// Absent cursor is the end — not a short page.
cursor = page.cursor ?? null;
if (!page.cursor) {
done = true;
puter.print('<b>— end of directory —</b>');
(async () => {
// Create 7 files so there is something to page through
for (let i = 1; i <= 7; i++) {
await puter.fs.write(`reports/report-${i}.txt`, `Report ${i}`, { createMissingParents: true });
}
});
output.innerHTML += 'Created 7 files in reports<br><br>';
await loadNextPage();
})();
button.addEventListener('click', loadNextPage);
</script>
</body>
</html>
@@ -1,26 +0,0 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<h3>Report generator</h3>
<script>
// Writing this quarter's report before the year/quarter folders exist.
(async () => {
const path = 'reports/2026/q3/summary.txt';
try {
await puter.fs.write(path, '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(path, 'Q3 summary', { createMissingParents: true });
puter.print('with createMissingParents: written<br><br>');
// mkdir takes the same three options, so next quarter is one call.
await puter.fs.mkdir('reports/2026/q4', { createMissingParents: true });
puter.print('q4 folder ready for next time');
})();
</script>
</body>
</html>
@@ -1,31 +0,0 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<h3>Upload endpoint</h3>
<button id="dedupe">Upload invoice.pdf (keep both)</button>
<button id="refuse">Upload invoice.pdf (refuse duplicates)</button>
<script>
// Two people send the same filename seconds apart. One policy keeps
// both; the other refuses the second and lets the caller rename it.
document.getElementById('dedupe').addEventListener('click', async () => {
const file = await puter.fs.write('uploads/invoice.pdf', 'data', {
dedupeName: true,
createMissingParents: true,
});
puter.print(`stored as ${file.name}<br>`);
});
document.getElementById('refuse').addEventListener('click', async () => {
try {
await puter.fs.write('uploads/invoice.pdf', 'data', {
overwrite: false,
createMissingParents: true,
});
puter.print('stored — the name was free<br>');
} catch (e) {
puter.print(`rejected, ask for a new name: ${e.code ?? e.message}<br>`);
}
});
</script>
</body>
</html>
+27 -4
View File
@@ -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/<your-app-id>/`, 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 `<input type="file">`, 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:
@@ -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`.