استاندارد سیستم فایل یک سیستم فایل خصوصی مبدا (OPFS) را به عنوان یک نقطه پایانی ذخیره سازی خصوصی در مبدا صفحه معرفی می کند و برای کاربر قابل مشاهده نیست که دسترسی اختیاری به نوع خاصی از فایل را فراهم می کند که برای عملکرد بسیار بهینه شده است.
پشتیبانی از مرورگر
سیستم فایل خصوصی مبدا توسط مرورگرهای مدرن پشتیبانی می شود و توسط گروه کاری فناوری کاربردی ابرمتن وب ( WHATWG ) در استاندارد زندگی فایل سیستم استاندارد شده است.
انگیزه
وقتی به فایلهای موجود در رایانه خود فکر میکنید، احتمالاً به یک سلسله مراتب فایل فکر میکنید: فایلهایی که در پوشههایی سازماندهی شدهاند که میتوانید با کاوشگر فایل سیستم عامل خود کاوش کنید. به عنوان مثال، در ویندوز، برای کاربری به نام Tom، لیست کارهای او ممکن است در C:\Users\Tom\Documents\ToDo.txt
زندگی کند. در این مثال، ToDo.txt
نام فایل است و Users
، Tom
و Documents
نام پوشه ها هستند. "C:" در ویندوز نشان دهنده دایرکتوری ریشه درایو است.
روش سنتی کار با فایل ها در وب
برای ویرایش لیست To Do در یک برنامه وب، این جریان معمول است:
- کاربر فایل را در سرور آپلود می کند یا آن را با
<input type="file">
روی کلاینت باز می کند . - کاربر تغییرات خود را انجام می دهد و سپس فایل به دست آمده را با
<a download="ToDo.txt>
تزریق شده دانلود می کند که به صورت برنامه نویسی روی آنclick()
از طریق جاوا اسکریپت. - برای باز کردن پوشهها، از یک ویژگی خاص در
<input type="file" webkitdirectory>
استفاده میکنید که با وجود نام اختصاصی آن، عملاً از مرورگر جهانی پشتیبانی میکند.
روش مدرن کار با فایل ها در وب
این جریان نشان دهنده نحوه تفکر کاربران در مورد ویرایش فایل ها نیست و به این معنی است که کاربران در نهایت نسخه های دانلود شده فایل های ورودی خود را دریافت می کنند. بنابراین، File System Access API سه روش انتخابگر را معرفی کرد - showOpenFilePicker()
، showSaveFilePicker()
و showDirectoryPicker()
- که دقیقاً همان کاری را انجام می دهند که نامشان نشان می دهد. آنها یک جریان را به صورت زیر فعال می کنند:
-
ToDo.txt
باshowOpenFilePicker()
باز کنید و یک شیFileSystemFileHandle
دریافت کنید. - از شی
FileSystemFileHandle
، با فراخوانی متدgetFile()
دسته فایل یکFile
دریافت کنید. - فایل را تغییر دهید، سپس
requestPermission({mode: 'readwrite'})
را روی دسته فراخوانی کنید. - اگر کاربر درخواست مجوز را پذیرفت، تغییرات را به فایل اصلی ذخیره کنید.
- از طرف دیگر،
showSaveFilePicker()
فراخوانی کنید و به کاربر اجازه دهید فایل جدیدی را انتخاب کند. (اگر کاربر فایلی را که قبلاً باز شده انتخاب کند، محتویات آن بازنویسی میشود.) برای ذخیرههای تکراری، میتوانید دسته فایل را در اطراف نگه دارید، بنابراین نیازی به نمایش مجدد گفتگوی ذخیره فایل ندارید.
محدودیت های کار با فایل ها در وب
فایلها و پوشههایی که از طریق این روشها قابل دسترسی هستند در جایی زندگی میکنند که میتوان آن را سیستم فایل قابل مشاهده توسط کاربر نامید. فایلهای ذخیرهشده از وب، و فایلهای اجرایی به طور خاص، با علامت وب مشخص میشوند، بنابراین یک هشدار اضافی وجود دارد که سیستم عامل میتواند قبل از اجرای یک فایل بالقوه خطرناک نشان دهد. به عنوان یک ویژگی امنیتی اضافی، فایلهای بهدستآمده از وب نیز توسط Safe Browsing محافظت میشوند، که برای سادگی و در چارچوب این مقاله، میتوانید آن را یک اسکن ویروس مبتنی بر ابر در نظر بگیرید. هنگامی که با استفاده از File System Access API دادهها را روی یک فایل مینویسید، نوشتهها در جای خود نیستند، اما از یک فایل موقت استفاده میکنند. خود فایل اصلاح نمی شود مگر اینکه تمام این بررسی های امنیتی را پشت سر بگذارد. همانطور که میتوانید تصور کنید، این کار علیرغم بهبودهایی که در جاهایی که ممکن است، به عنوان مثال، در macOS اعمال میشود، عملیات فایل را نسبتاً کند میکند. با این حال، هر فراخوانی write()
مستقل است، بنابراین در زیر هود فایل را باز میکند، به دنبال افست داده شده میگردد و در نهایت دادهها را مینویسد.
فایل ها به عنوان پایه پردازش
در عین حال، فایل ها راه بسیار خوبی برای ثبت داده ها هستند. به عنوان مثال، SQLite کل پایگاه داده را در یک فایل ذخیره می کند. مثال دیگر mipmaps هستند که در پردازش تصویر استفاده می شوند. Mipmaps توالیهای از پیش محاسبهشده و بهینهسازیشدهای از تصاویر هستند که هر کدام از آنها نمایشی با وضوح کمتر از تصویر قبلی است که باعث میشود بسیاری از عملیاتها مانند زوم کردن سریعتر شوند. بنابراین چگونه برنامه های کاربردی وب می توانند از مزایای فایل ها بهره مند شوند، اما بدون هزینه های عملکرد پردازش فایل های مبتنی بر وب؟ پاسخ سیستم فایل خصوصی مبدا است.
سیستم فایل خصوصی قابل مشاهده توسط کاربر در مقابل مبدا
برخلاف سیستم فایل قابل مشاهده توسط کاربر که با استفاده از کاوشگر فایل سیستم عامل مرور میشود، با فایلها و پوشههایی که میتوانید بخوانید، بنویسید، جابهجا کنید، و نامگذاری کنید، سیستم فایل خصوصی مبدأ برای کاربران قابل مشاهده نیست. فایلها و پوشهها در سیستم فایل خصوصی مبدا، همانطور که از نام آن پیداست، خصوصی هستند و به طور مشخص، خصوصی برای مبدا یک سایت هستند. منشا یک صفحه را با تایپ location.origin
در DevTools Console کشف کنید. به عنوان مثال، مبدا صفحه https://developer.chrome.com/articles/
https://developer.chrome.com
است (یعنی قسمت /articles
بخشی از مبدا نیست ). شما می توانید در مورد تئوری منشاء در درک "همان سایت" و "همان منبع" بیشتر بخوانید. همه صفحاتی که منشا یکسانی دارند میتوانند دادههای سیستم فایل خصوصی مبدا یکسانی را ببینند، بنابراین https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/
میتوانند جزئیات مشابه مثال قبلی را ببینند. هر مبدأ سیستم فایل خصوصی منشأ مستقل خود را دارد، به این معنی که سیستم فایل خصوصی مبدا https://developer.chrome.com
کاملاً از مثلاً https://web.dev
متمایز است. در ویندوز، دایرکتوری ریشه فایل سیستم قابل مشاهده توسط کاربر C:\\
است. معادل سیستم فایل خصوصی مبدا یک دایرکتوری ریشه خالی در ابتدا برای هر مبدا است که با فراخوانی روش ناهمزمان navigator.storage.getDirectory()
قابل دسترسی است. برای مقایسه سیستم فایل قابل مشاهده توسط کاربر و سیستم فایل خصوصی مبدا، به نمودار زیر مراجعه کنید. این نمودار نشان می دهد که به غیر از دایرکتوری ریشه، همه چیزهای دیگر از نظر مفهومی یکسان هستند، با سلسله مراتبی از فایل ها و پوشه ها برای سازماندهی و مرتب سازی در صورت نیاز برای داده ها و نیازهای ذخیره سازی شما.
مشخصات سیستم فایل خصوصی مبدا
درست مانند سایر مکانیسم های ذخیره سازی در مرورگر (به عنوان مثال، localStorage یا IndexedDB )، سیستم فایل خصوصی مبدا مشمول محدودیت های سهمیه مرورگر است. هنگامی که یک کاربر تمام داده های مرور یا تمام داده های سایت را پاک می کند، سیستم فایل خصوصی مبدا نیز حذف می شود. navigator.storage.estimate()
را فراخوانی کنید و در شیء پاسخ، ورودی usage
را ببینید تا ببینید برنامه شما در حال حاضر چقدر فضای ذخیرهسازی مصرف میکند، که توسط مکانیسم ذخیرهسازی در شی usageDetails
، جایی که میخواهید به طور خاص به ورودی fileSystem
نگاه کنید، تجزیه میشود. . از آنجایی که سیستم فایل خصوصی مبدا برای کاربر قابل مشاهده نیست، هیچ درخواست مجوز و هیچ بررسی Safe Browsing وجود ندارد.
دسترسی به دایرکتوری ریشه
برای دسترسی به دایرکتوری ریشه، دستور زیر را اجرا کنید. در نهایت یک دسته دایرکتوری خالی، به طور خاص، یک FileSystemDirectoryHandle
خواهید داشت.
const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);
موضوع اصلی یا Web Worker
دو راه برای استفاده از سیستم فایل خصوصی مبدا وجود دارد: در رشته اصلی یا در Web Worker . Web Workers نمی توانند رشته اصلی را مسدود کنند، به این معنی که در این زمینه APIها می توانند همزمان باشند، الگویی که عموماً در رشته اصلی مجاز نیست. 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 بروید. در غیر این صورت، می توانید ادامه مطلب را بخوانید.
از سیستم فایل خصوصی مبدا در موضوع اصلی استفاده کنید
فایل ها و پوشه های جدید ایجاد کنید
هنگامی که یک پوشه root دارید، به ترتیب با استفاده از متد 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()
یا 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()
handle فایل یا دایرکتوری آنها حذف کنید. برای حذف یک پوشه شامل همه زیرپوشه ها، گزینه {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()) {}
به صورت بازگشتی محتویات یک پوشه و همه زیرپوشه ها را فهرست کنید
برخورد با حلقه های ناهمزمان و توابع جفت شده با بازگشت به راحتی اشتباه می شود. تابع زیر می تواند به عنوان نقطه شروعی برای فهرست کردن محتویات یک پوشه و همه زیرپوشه های آن، از جمله همه فایل ها و اندازه آنها باشد. اگر به اندازه فایلها نیاز handle
handle.getFile()
میتوانید تابع را با استفاده از directoryEntryPromises.push
، سادهسازی کنید.
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 نمی توانند رشته اصلی را مسدود کنند، به همین دلیل است که در این زمینه روش های همزمان مجاز هستند.
دریافت یک دسته دسترسی همزمان
نقطه ورود به سریعترین عملیات ممکن فایل یک 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 اصلی قرار دارد، اما نه در موضوع 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);
}
اشکال زدایی سیستم فایل خصوصی مبدا
تا زمانی که پشتیبانی DevTools داخلی اضافه نشود (به crbug/1284595 مراجعه کنید)، از افزونه OPFS Explorer Chrome برای اشکال زدایی سیستم فایل خصوصی اصلی استفاده کنید. اسکرین شات بالا از بخش ایجاد فایلها و پوشههای جدید مستقیماً از پسوند گرفته شده است.
پس از نصب افزونه، Chrome DevTools را باز کنید، تب OPFS Explorer را انتخاب کنید و سپس آماده بررسی سلسله مراتب فایل هستید. با کلیک کردن روی نام فایل، فایلها را از سیستم فایل خصوصی مبدا در سیستم فایل قابل مشاهده کاربر ذخیره کنید و با کلیک کردن روی نماد سطل زباله، فایلها و پوشهها را حذف کنید.
نسخه ی نمایشی
سیستم فایل خصوصی مبدا را در عمل مشاهده کنید (اگر پسوند OPFS Explorer را نصب کرده باشید) در نسخه نمایشی که از آن به عنوان پشتیبان برای پایگاه داده SQLite کامپایل شده در WebAssembly استفاده می کند. حتماً کد منبع را در Glitch بررسی کنید. توجه داشته باشید که چگونه نسخه تعبیهشده زیر از باطن سیستم فایل خصوصی مبدا استفاده نمیکند (زیرا iframe از مبدا متقاطع است)، اما وقتی نسخه نمایشی را در یک برگه جداگانه باز میکنید، از آن استفاده میکند.
نتیجه گیری
سیستم فایل خصوصی مبدا، همانطور که توسط WHATWG مشخص شده است، نحوه استفاده و تعامل ما با فایل ها را در وب شکل داده است. موارد استفاده جدیدی را فعال کرده است که دستیابی به آنها با سیستم فایل قابل مشاهده توسط کاربر غیرممکن بود. همه فروشندگان بزرگ مرورگرها - اپل، موزیلا و گوگل - در کنار هم هستند و یک چشم انداز مشترک دارند. توسعه سیستم فایل خصوصی مبدا یک تلاش مشترک است و بازخورد توسعه دهندگان و کاربران برای پیشرفت آن ضروری است. همانطور که به اصلاح و بهبود استاندارد ادامه می دهیم، بازخورد در مورد مخزن whatwg/fs در قالب Issues یا Pull Requests پذیرفته می شود.
لینک های مرتبط
- مشخصات استاندارد فایل سیستم
- مخزن استاندارد فایل سیستم
- پست File System API with Origin Private File System WebKit
- پسوند OPFS Explorer
قدردانی ها
این مقاله توسط آستین سالی ، اتین نوئل و ریچل اندرو بررسی شده است. تصویر قهرمان توسط کریستینا رامپ در Unsplash .