File System Standard כולל מערכת קבצים פרטית (OPFS) המבוססת על מקור, כנקודת קצה לאחסון פרטית למקור הדף ולא גלויה למשתמש. היא מספקת גישה אופציונלית לסוג קובץ מיוחד שעבר אופטימיזציה לביצועים.
תמיכה בדפדפנים
מערכת הקבצים הפרטית של המקור נתמכת על ידי דפדפנים מודרניים, והיא מוגדרת על ידי קבוצת העבודה של Web Hypertext Application Technology Group (WHEREWG) במסגרת File System Living Standard.
למה בחרנו לעשות זאת?
כשחושבים על קבצים במחשב, כנראה צריך לחשוב על היררכיית קבצים: קבצים המסודרים בתיקיות, שניתן לעיין בהם באמצעות סייר הקבצים של מערכת ההפעלה. לדוגמה, ב-Windows, עבור משתמש בשם תמיר, יכול להיות שרשימת המשימות שלו נמצאת בC:\Users\Tom\Documents\ToDo.txt
. בדוגמה הזו, ToDo.txt
הוא שם הקובץ, ו-Users
, Tom
ו-Documents
הם שמות התיקיות. 'C:' ב-Windows מייצג את תיקיית השורש של הכונן.
הדרך המסורתית לעבוד עם קבצים באינטרנט
כך עורכים את רשימת המשימות באפליקציית אינטרנט:
- המשתמש מעלה את הקובץ לשרת או פותח אותו אצל הלקוח באמצעות
<input type="file">
. - המשתמש מבצע את השינויים ואז מוריד את הקובץ שנוצר באמצעות
<a download="ToDo.txt>
שהוזרק באופן פרוגרמטיclick()
באמצעות JavaScript. - כדי לפתוח תיקיות, צריך להשתמש במאפיין מיוחד ב-
<input type="file" webkitdirectory>
. למרות השם הקנייני שלו, יש תמיכה בדפדפן כמעט אוניברסלי.
דרך מודרנית לעבוד עם קבצים באינטרנט
התהליך הזה לא מייצג את האופן שבו המשתמשים חושבים על עריכת קבצים, וכתוצאה מכך יש למשתמשים עותקים של קובצי הקלט שלהם שהורידו. לכן, File System Access API הוסיפה שלוש שיטות בחירה – showOpenFilePicker()
, showSaveFilePicker()
ו-showDirectoryPicker()
– שעושות בדיוק את מה שהשם שלהן מרמז. הן מאפשרות זרימה באופן הבא:
- פותחים את
ToDo.txt
באמצעותshowOpenFilePicker()
ומקבלים אובייקטFileSystemFileHandle
. - מהאובייקט
FileSystemFileHandle
, מקבליםFile
על ידי קריאה ל-methodgetFile()
של נקודת האחיזה של הקובץ. - משנים את הקובץ ואז קוראים לפונקציה
requestPermission({mode: 'readwrite'})
בכינוי. - אם המשתמש מאשר את בקשת ההרשאה, שומרים את השינויים בחזרה בקובץ המקורי.
- לחלופין, אפשר להתקשר ל-
showSaveFilePicker()
ולתת למשתמש לבחור קובץ חדש. (אם המשתמש יבחר קובץ שנפתח בעבר, התוכן שלו יוחלף). לשמירת קבצים חוזרים, אפשר לשמור את נקודת האחיזה של הקובץ כך שלא יהיה צורך להציג שוב את תיבת הדו-שיח של שמירת הקובץ.
הגבלות עבודה עם קבצים באינטרנט
תיקיות וקבצים שניתן לגשת אליהם באמצעות השיטות האלה זמינים במערכת קבצים גלויה למשתמש. קבצים שנשמרו מהאינטרנט, וקובצי הפעלה באופן ספציפי, מסומנים בסימן האינטרנט, כך שקיימת אזהרה נוספת שמערכת ההפעלה יכולה להציג לפני הפעלה של קובץ שעלול להיות מסוכן. כאמצעי אבטחה נוסף, קבצים שמתקבלים מהאינטרנט מוגנים גם באמצעות התכונה גלישה בטוחה. לשם כך, כדי לשמור על פשטות ובהקשר של המאמר הזה, אפשר להתייחס אליה כסריקת וירוסים מבוססת-ענן. כשכותבים נתונים לקובץ באמצעות File System Access API, הכתיבה לא נקבעת במקום, אלא נעשה שימוש בקובץ זמני. הקובץ עצמו לא ישתנה אלא אם הוא יעבור את כל בדיקות האבטחה האלה. כפי שאפשר לדמיין, עבודה זו מאטה את פעולת הקבצים באופן יחסי, למרות השיפורים שיושמו במידת האפשר, לדוגמה ב-macOS. בכל זאת, כל קריאה ל-write()
עומדת בפני עצמה, כך שמתחת למכסה זו הקובץ פותח את הקובץ, מנסה להגיע לקיזוז הנתון ולבסוף כותב נתונים.
קבצים כבסיס לעיבוד
במקביל, קבצים הם דרך מצוינת לתיעוד נתונים. לדוגמה, SQLite מאחסן מסדי נתונים שלמים בקובץ יחיד. דוגמה נוספת היא mipmaps, שמשמש לעיבוד תמונות. מפות Mipmaps מחושבות מראש ומותאמות לרצפים של תמונות שעברו אופטימיזציה. כל אחד מהם מייצג רזולוציה נמוכה יותר בהדרגה של הפריט הקודם. כתוצאה מכך, ניתן לבצע פעולות רבות, כמו שינוי מרחק התצוגה, מהר יותר. אז איך אפליקציות אינטרנט יכולות ליהנות מהיתרונות של קבצים אבל בלי עלויות הביצועים של עיבוד קבצים מבוסס-אינטרנט? התשובה היא מערכת הקבצים הפרטיים של המקור.
מערכת הקבצים הפרטית והגלויה למשתמש לעומת מערכת הקבצים הפרטית של המקור
בניגוד למערכת הקבצים הגלויה למשתמש, שגולשים בה באמצעות סייר הקבצים של מערכת ההפעלה, קבצים ותיקיות שאפשר לקרוא, לכתוב, להעביר ולשנות את השם שלהם, מערכת הקבצים הפרטית המקורית לא אמורה להיות גלויה למשתמשים. קבצים ותיקיות במערכת הקבצים הפרטית של המקור, כפי שמרמז השם, הם פרטיים ובאופן קונקרטי יותר, הם פרטיים למקור של האתר. כדי לגלות את מקור הדף, מקלידים location.origin
במסוף כלי הפיתוח. לדוגמה, המקור של הדף https://developer.chrome.com/articles/
הוא https://developer.chrome.com
(כלומר, החלק /articles
אינו חלק מהמקור). אפשר לקרוא מידע נוסף על תיאוריית המקורות במאמר הבנת "אותו אתר" ו-'same-origin'. כל הדפים שיש להם אותו מקור יכולים לראות את אותו נתוני מקור של מערכת קבצים פרטית, כך ש-https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/
יכול לראות את אותם פרטים כמו בדוגמה הקודמת. לכל מקור יש מערכת קבצים פרטית ממקור עצמאי. כלומר, מערכת הקבצים הפרטית של https://developer.chrome.com
שהמקור שלה שונה לגמרי ממערכת הקבצים https://web.dev
, למשל. ב-Windows, ספריית הבסיס של מערכת הקבצים הגלויה למשתמשים היא C:\\
.
הגרסה המקבילה למערכת הקבצים הפרטית של המקור היא ספריית בסיס ריקה בהתחלה לכל מקור שהגישה אליו מתבצעת באמצעות קריאה לשיטה האסינכרונית
navigator.storage.getDirectory()
להשוואה בין מערכת הקבצים הגלויה למשתמש לבין מערכת הקבצים הפרטית המקורית, ראו את התרשים הבא. בדיאגרמה רואים שחוץ מספריית השורש, כל השאר נשאר זהה מבחינה רעיונית, עם היררכיה של קבצים ותיקיות לארגון ולארגון לפי הצורך בהתאם לצורכי הנתונים והאחסון שלכם.
פרטים ספציפיים על מערכת הקבצים הפרטית המקורית
בדומה למנגנוני אחסון אחרים בדפדפן (לדוגמה, localStorage או IndexedDB), מערכת הקבצים הפרטית של המקור כפופה להגבלות על מכסות הדפדפן. כשמשתמש מוחק את כל נתוני הגלישה או את כל נתוני האתר, גם מערכת הקבצים הפרטית המקורית נמחקת. קוראים לפונקציה navigator.storage.estimate()
, ובאובייקט התשובה שמתקבלת מופיעה הרשומה usage
, כדי לבדוק כמה נפח אחסון האפליקציה כבר צורכת, בחלוקה לפי מנגנון האחסון באובייקט usageDetails
, שבו רוצים לעיין ספציפית ברשומה fileSystem
. מערכת הקבצים הפרטית של המקור לא גלויה למשתמש, ולכן לא מוצגות בקשות להרשאות ולא בדיקות של הגלישה הבטוחה.
קבלת גישה לתיקיית השורש
כדי לקבל גישה לתיקיית השורש, מריצים את הפקודה הבאה. התוצאה תהיה כינוי ריק של ספרייה, ובאופן ספציפי יותר, FileSystemDirectoryHandle
.
const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);
thread ראשי או Web Worker
יש שתי דרכים להשתמש במערכת הקבצים הפרטית של המקור: בשרשור הראשי או ב-Web Worker. Web Workers לא יכול לחסום את ה-thread הראשי. כלומר, במקרה הזה ממשקי API יכולים להיות סינכרוניים, מכיוון שבדרך כלל זה דפוס שאסור להשתמש בו ב-thread הראשי. ממשקי API סינכרוניים יכולים לפעול מהר יותר כי הם לא צריכים להתמודד עם הבטחות, ופעולות הקבצים בדרך כלל מסונכרנות בשפות כמו C שאפשר להדר אותן ל-WebAssembly.
// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);
אם אתם צריכים את הפעולות המהירות ביותר בקבצים, או אם אתם מטפלים ב-WebAssembly, תוכלו לדלג למטה לקטע שימוש במערכת הקבצים הפרטית של המקור ב-Web Worker. אחרת, אפשר להמשיך לקרוא.
שימוש במערכת הקבצים הפרטית של המקור ב-thread הראשי
יצירת קבצים ותיקיות חדשים
אחרי שיוצרים תיקיית בסיס, יוצרים קבצים ותיקיות באמצעות השיטות getFileHandle()
ו-getDirectoryHandle()
בהתאמה. לאחר ההעברה של {create: true}
, המערכת תיצור את הקובץ או התיקייה אם הם לא קיימים. כדי ליצור היררכיה של קבצים, צריך להפעיל את הפונקציות האלה באמצעות ספרייה חדשה שנוצרה כנקודת ההתחלה.
const fileHandle = await opfsRoot
.getFileHandle('my first file', {create: true});
const directoryHandle = await opfsRoot
.getDirectoryHandle('my first folder', {create: true});
const nestedFileHandle = await directoryHandle
.getFileHandle('my first nested file', {create: true});
const nestedDirectoryHandle = await directoryHandle
.getDirectoryHandle('my first nested folder', {create: true});
גישה לתיקיות ולקבצים קיימים
אם יודעים את השמות שלהם, אפשר לגשת לקבצים ולתיקיות שנוצרו בעבר על ידי קריאה ל-getFileHandle()
או ל-method getDirectoryHandle()
, והעברת שם הקובץ או התיקייה.
const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
.getDirectoryHandle('my first folder');
אחזור הקובץ המשויך לכינוי של קובץ לקריאה
FileSystemFileHandle
מייצג קובץ במערכת הקבצים. כדי לקבל את File
המשויך, צריך להשתמש בשיטה getFile()
. אובייקט File
הוא סוג ספציפי של Blob
, ואפשר להשתמש בו בכל הקשר שבו Blob
יכול. באופן ספציפי, FileReader
, URL.createObjectURL()
, createImageBitmap()
ו-XMLHttpRequest.send()
מקבלים גם את Blobs
וגם את Files
. אם כן, קבלת File
מ-FileSystemFileHandle
'חינם' את הנתונים, כדי שתוכל לגשת אליהם ולהפוך אותם לזמינים למערכת הקבצים הגלויה למשתמש.
const file = await fileHandle.getFile();
console.log(await file.text());
כתיבה לקובץ באמצעות סטרימינג
אפשר להעביר נתונים בסטרימינג לקובץ באמצעות קריאה ל-createWritable()
, שיוצרת FileSystemWritableFileStream
כדי שתוכלו write()
את התוכן. בסיום, תצטרכו close()
את השידור.
const contents = 'Some text';
// Get a writable stream.
const writable = await fileHandle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the stream, which persists the contents.
await writable.close();
מחיקה של קבצים ותיקיות
כדי למחוק קבצים ותיקיות, קוראים לשיטה הספציפית של מזהה הקובץ או הספרייה remove()
. כדי למחוק תיקייה, כולל כל תיקיות המשנה, מעבירים את האפשרות {recursive: true}
.
await fileHandle.remove();
await directoryHandle.remove({recursive: true});
לחלופין, אם יודעים את השם של הקובץ או התיקייה שרוצים למחוק בספרייה, משתמשים בשיטה removeEntry()
.
directoryHandle.removeEntry('my first nested file');
העברה של קבצים ותיקיות ושינוי השם שלהם
משנים את השמות של הקבצים והתיקיות ומעבירים אותם באמצעות השיטה move()
. העברה ושינוי שם יכולים להתרחש יחד או בנפרד.
// Rename a file.
await fileHandle.move('my first renamed file');
// Move a file to another directory.
await fileHandle.move(nestedDirectoryHandle);
// Move a file to another directory and rename it.
await fileHandle
.move(nestedDirectoryHandle, 'my first renamed and now nested file');
זיהוי נתיב של קובץ או תיקייה
כדי ללמוד איפה קובץ או תיקייה מסוימים נמצאים ביחס לספריית עזר, אפשר להשתמש בשיטה resolve()
ולהעביר אותה כארגומנט FileSystemHandle
. כדי לקבל את הנתיב המלא של קובץ או תיקייה במערכת הקבצים הפרטית של המקור, צריך להשתמש בספריית השורש בתור ספריית העזר שהתקבלה דרך navigator.storage.getDirectory()
.
const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.
בודקים אם שני כינויים של קבצים או תיקיות מפנים לאותו קובץ או תיקייה
לפעמים יש לכם שני כינויים ואתם לא יודעים אם הם מפנים לאותו קובץ או לאותו תיקייה. כדי לבדוק אם זה המצב, משתמשים בשיטה isSameEntry()
.
fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.
הצגת רשימה של תוכן התיקייה
FileSystemDirectoryHandle
הוא איטרטור אסינכרוני שניתן לחזור עליו באמצעות לולאה של for await…of
. בתור איטרטור אסינכרוני, הוא תומך גם בשיטות entries()
, values()
ו-keys()
, שמתוכן אפשר לבחור בהתאם למידע שדרוש לכם:
for await (let [name, handle] of directoryHandle) {}
for await (let [name, handle] of directoryHandle.entries()) {}
for await (let handle of directoryHandle.values()) {}
for await (let name of directoryHandle.keys()) {}
הצגה באופן רקורסיבי של תוכן התיקייה וכל תיקיות המשנה
קל לטעות בהתמודדות עם פונקציות ולולאות אסינכרוניות בשילוב עם רקורסיה. הפונקציה שבהמשך יכולה לשמש כנקודת התחלה להצגת התוכן של תיקייה וכל תיקיות המשנה שלה, כולל כל הקבצים והגודל שלהם. אפשר לפשט את הפונקציה אם אתם לא צריכים את גודל הקבצים לפי, כאשר כתוב directoryEntryPromises.push
, לא לדחוף את ההבטחה handle.getFile()
אלא את handle
ישירות.
const getDirectoryEntriesRecursive = async (
directoryHandle,
relativePath = '.',
) => {
const fileHandles = [];
const directoryHandles = [];
const entries = {};
// Get an iterator of the files and folders in the directory.
const directoryIterator = directoryHandle.values();
const directoryEntryPromises = [];
for await (const handle of directoryIterator) {
const nestedPath = `${relativePath}/${handle.name}`;
if (handle.kind === 'file') {
fileHandles.push({ handle, nestedPath });
directoryEntryPromises.push(
handle.getFile().then((file) => {
return {
name: handle.name,
kind: handle.kind,
size: file.size,
type: file.type,
lastModified: file.lastModified,
relativePath: nestedPath,
handle
};
}),
);
} else if (handle.kind === 'directory') {
directoryHandles.push({ handle, nestedPath });
directoryEntryPromises.push(
(async () => {
return {
name: handle.name,
kind: handle.kind,
relativePath: nestedPath,
entries:
await getDirectoryEntriesRecursive(handle, nestedPath),
handle,
};
})(),
);
}
}
const directoryEntries = await Promise.all(directoryEntryPromises);
directoryEntries.forEach((directoryEntry) => {
entries[directoryEntry.name] = directoryEntry;
});
return entries;
};
שימוש במערכת הקבצים הפרטית של המקור ב-Web Worker
כפי שתיארנו קודם, Web Workers לא יכולים לחסום את ה-thread הראשי, ולכן בהקשר הזה אפשר להשתמש בשיטות סינכרוניות.
קבלת מזהה גישה סינכרוני
נקודת הכניסה לפעולות המהירות ביותר שאפשר לבצע בקובץ היא FileSystemSyncAccessHandle
, שמתקבלת מ-FileSystemFileHandle
רגיל באמצעות קריאה ל-createSyncAccessHandle()
.
const fileHandle = await opfsRoot
.getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();
שיטות מסונכרנות שהוגדרו לקבצים
ברגע שיש לכם נקודת אחיזה סינכרונית לגישה, תוכלו לגשת לשיטות קבצים במקום מהיר שכולן מסונכרנות.
getSize()
: הפונקציה מחזירה את גודל הקובץ בבייטים.write()
: כתיבת התוכן של מאגר הנתונים הזמני בקובץ, אופציונלי בקיזוז נתון, והחזרת מספר הבייטים שנכתבו. בדיקת המספר המוחזר של הבייטים שנכתבו מאפשרת למתקשרים לזהות שגיאות וכתיבה חלקית ולטפל בהן.read()
: קריאת התוכן של הקובץ לתוך מאגר נתונים זמני, אופציונלי בהיסט נתון.truncate()
: שינוי הגודל של הקובץ לגודל הנתון.flush()
: מוודא שהתוכן של הקובץ מכיל את כל השינויים שבוצעו באמצעותwrite()
.close()
: הלחצן סוגר את נקודת האחיזה לגישה.
בהמשך מוצגת דוגמה שמשתמשת בכל השיטות שצוינו למעלה.
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('fast', {create: true});
const accessHandle = await fileHandle.createSyncAccessHandle();
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode('Some text');
// Write the content at the beginning of the file.
accessHandle.write(content, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();
// Encode more content to write to the file.
const moreContent = textEncoder.encode('More content');
// Write the content at the end of the file.
accessHandle.write(moreContent, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();
// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));
// Read the entire file into the data view.
accessHandle.read(dataView);
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));
// Read starting at offset 9 into the data view.
accessHandle.read(dataView, {at: 9});
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));
// Truncate the file after 4 bytes.
accessHandle.truncate(4);
העתקת קובץ ממערכת הקבצים הפרטית של המקור למערכת הקבצים הגלויה למשתמשים
כפי שצוין למעלה, לא ניתן להעביר קבצים ממערכת הקבצים הפרטית המקורית למערכת הקבצים שגלויה למשתמשים, אבל אפשר להעתיק קבצים. מכיוון ש-showSaveFilePicker()
חשוף רק ב-thread הראשי, אבל לא ב-thread של Worker, חשוב להקפיד להריץ את הקוד שם.
// On the main thread, not in the Worker. This assumes
// `fileHandle` is the `FileSystemFileHandle` you obtained
// the `FileSystemSyncAccessHandle` from in the Worker
// thread. Be sure to close the file in the Worker thread first.
const fileHandle = await opfsRoot.getFileHandle('fast');
try {
// Obtain a file handle to a new file in the user-visible file system
// with the same name as the file in the origin private file system.
const saveHandle = await showSaveFilePicker({
suggestedName: fileHandle.name || ''
});
const writable = await saveHandle.createWritable();
await writable.write(await fileHandle.getFile());
await writable.close();
} catch (err) {
console.error(err.name, err.message);
}
ניפוי באגים במערכת הקבצים הפרטית המקורית
עד שתתווסף תמיכה מובנית בכלי הפיתוח (ראו crbug/1284595), תוכלו להשתמש בתוסף OPFS Explorer ל-Chrome כדי לנפות באגים במערכת הקבצים הפרטית של המקור. צילום המסך שלמעלה מהקטע יצירת קבצים ותיקיות חדשים נלקח ישירות מהתוסף דרך אגב.
לאחר שמתקינים את התוסף, פותחים את כלי הפיתוח ל-Chrome, בוחרים בכרטיסייה OPFS Explorer. לאחר מכן תוכלו לבדוק את היררכיית הקבצים. כדי לשמור קבצים ממערכת הקבצים הפרטית המקורית במערכת הקבצים הגלויה למשתמשים, לוחצים על שם הקובץ ומוחקים קבצים ותיקיות על ידי לחיצה על סמל פח האשפה.
הדגמה (דמו)
אפשר לראות את מערכת הקבצים הפרטית של המקור בפעולה (אם מתקינים את התוסף OPFS Explorer) בהדגמה שמשתמשת בה כקצה עורפי למסד נתונים SQLite שהורכב ל-WebAssembly. חשוב לבדוק את קוד המקור ב-Glitch. שימו לב שהגרסה המוטמעת שבהמשך לא משתמשת בקצה העורפי של מערכת הקבצים הפרטית של המקור (כי ה-iframe הוא ממקורות שונים), אבל כשפותחים את ההדגמה בכרטיסייה נפרדת, היא כן עושה זאת.
מסקנות
מערכת הקבצים הפרטיים של המקור, כפי שצוין ב-whatWG, עיצבה את האופן שבו אנחנו משתמשים בקבצים באינטרנט ויוצרים איתם אינטראקציה. לאחר מכן הופעלו תרחישים חדשים לדוגמה שלא ניתן היה להשיג באמצעות מערכת הקבצים הגלויה למשתמשים. כל ספקי הדפדפנים המובילים – Apple, Mozilla ו-Google – משתתפים ביוזמה וחולקים חזון משותף. הפיתוח של מערכת הקבצים הפרטית של המקור הוא מאוד מאמץ, והמשוב מהמפתחים ומהמשתמשים חיוני להתקדמות. אנחנו ממשיכים לשפר ולשפר את התקן הזה, ולכן נשמח לקבל משוב על מאגר whatwg/fs בפורמט 'בעיות' או 'בקשות משיכה' (pull requests).
קישורים רלוונטיים
- המפרט של File System Standard
- מאגר רגיל של File System
- File System API עם פוסט של Origin Private System WebKit
- תוסף OPFS Explorer
אישורים
המאמר הזה נבדקה על ידי אוסטין סולי, אטיין נואנל ורייצ'ל אנדרו. התמונה הראשית (Hero) של כריסטינה רואמפ (Christina Rumpf) בסדרת Unצופים.