Watch a Folder for New Files
Your app can read and write files in the user's Puter account with
puter.fs, the Puter file system API. A common feature built on it is
an import folder. The user adds a file to a folder, and your app reads the file
and saves what's in it.
This recipe builds a CSV importer. When the user adds a .csv file to the
inbox folder, the app reads it and writes a summary file next to it. To know
when a file is added, the app watches the folder with the Events API.
It builds on Watch for Changes, which shows how
to watch a folder to update what your app shows.
Your app processes the files while it's open, so it can use any library, such as a CSV parser. Files added while your app is closed are processed the next time it opens. To process them while your app is closed, see Run Code in the Background.
Watch the Folder
First, create the folder with puter.fs.mkdir(). Then subscribe
to it with puter.events.onLocal(), which calls a function
each time something in the folder changes. It takes a subject, which is a
string that says what to watch, and the function to call. The subject
fs:inbox/*.csv:add matches new .csv files in inbox:
async function summarize(path) {
const blob = await puter.fs.read(path);
const text = await blob.text();
const name = path.split('/').pop().replace(/\.csv$/, '');
await puter.fs.write(`inbox/${name}.summary.json`, JSON.stringify({
rows: text.trim().split('\n').length - 1,
at: Date.now(),
}));
}
await puter.fs.mkdir('inbox', { createMissingParents: true });
await puter.events.onLocal('fs:inbox/*.csv:add', ({ event }) => summarize(event.path));
The function is called with { event }, and event.path is the full path of
the new file. The summarize() function reads the file with
puter.fs.read() and writes a summary with
puter.fs.write(). In your app, this is where you'd parse the
rows and save them. Add a CSV file to inbox, and its summary appears in the
same folder.
Each app has its own folder in the user's account, and a relative path such as
inbox is inside it. A relative path works the same way in a subject as in
puter.fs. If the folder already exists,
mkdir() returns it, so the code works every time the app opens.
In the subject, * matches any file name, and the :add suffix limits the
subscription to new files. The summary ends in .json, so the subject doesn't
match it, and writing it doesn't call the function again. All the patterns and
suffixes are listed on Subjects and events.
On a website, the user has to be signed in. If nobody is,
onLocal() opens the Puter sign-in window.
Process Files Moved Into the Folder
Uploads and copies arrive as add. A file moved in from another folder, for
example by dragging it on the Puter desktop, arrives as a move, so the :add
subject misses it. To process both, leave the suffix off and check event.op:
await puter.events.onLocal('fs:inbox/*.csv', ({ event }) => {
const arrived = event.op === 'add' || (event.op === 'move' && !event.from);
if (!arrived) return;
return summarize(event.path);
});
A move has a from field when the file was already inside the watched
folder, for example when it's renamed inside inbox. The function skips those,
along with write and remove events. A file moved in from another folder has
no from.
This depends on inbox existing when you subscribe. A subscription on a folder
that doesn't exist yet watches from the nearest folder above it, so a file moved
in from a folder next to inbox would have a from. That's why the code
creates inbox first.
Catch Up on Missed Files
Files added while your app is closed send no events. Sometimes an event also
can't be delivered, for example when the connection drops while a laptop
sleeps. The function then gets a gap marker in its place. A gap marker is an
event with op: 'gap' and no path, so it doesn't say which files were added.
Watch for Changes has more on gaps.
To catch up, list the folder with puter.fs.readdir() and
process every CSV file that has no summary yet. Run it once after subscribing,
and again on each gap:
async function catchUp() {
const items = await puter.fs.readdir('inbox');
const done = new Set(items
.filter(i => i.name.endsWith('.summary.json'))
.map(i => i.name.replace(/\.summary\.json$/, '')));
for (const item of items) {
if (!item.name.endsWith('.csv')) continue;
if (done.has(item.name.replace(/\.csv$/, ''))) continue;
await summarize(item.path);
}
}
function handler({ event }) {
if (event.op === 'gap') return catchUp();
const arrived = event.op === 'add' || (event.op === 'move' && !event.from);
if (!arrived) return;
return summarize(event.path);
}
const sub = await puter.events.onLocal('fs:inbox/*.csv', handler);
await catchUp();
The summary files record which CSV files are already processed, so the app doesn't need to store that anywhere else.
The code subscribes before the first catchUp(), so a file added during the
listing still reaches the function. That file can be processed twice, so make
processing safe to repeat. Here the second summary overwrites the first. In
your app, save the rows under a key made from the file name, so processing the
file again replaces its rows instead of adding a second copy.
Stop Watching
To stop, call off() on the subscription, for example when the
user leaves the screen that shows the inbox:
// Later, when the user leaves this screen:
await sub.off();
Closing the tab ends the subscription on its own.
Full Code
async function summarize(path) {
const blob = await puter.fs.read(path);
const text = await blob.text();
const name = path.split('/').pop().replace(/\.csv$/, '');
await puter.fs.write(`inbox/${name}.summary.json`, JSON.stringify({
rows: text.trim().split('\n').length - 1,
at: Date.now(),
}));
}
async function catchUp() {
const items = await puter.fs.readdir('inbox');
const done = new Set(items
.filter(i => i.name.endsWith('.summary.json'))
.map(i => i.name.replace(/\.summary\.json$/, '')));
for (const item of items) {
if (!item.name.endsWith('.csv')) continue;
if (done.has(item.name.replace(/\.csv$/, ''))) continue;
await summarize(item.path);
}
}
function handler({ event }) {
if (event.op === 'gap') return catchUp();
const arrived = event.op === 'add' || (event.op === 'move' && !event.from);
if (!arrived) return;
return summarize(event.path);
}
await puter.fs.mkdir('inbox', { createMissingParents: true });
const sub = await puter.events.onLocal('fs:inbox/*.csv', handler);
await catchUp();
// Later, when the user leaves this screen:
await sub.off();