sshfs user@remote:~/ /mnt/remoteroot
çalıştırdığınızda ne olacağını hiç merak ettiniz mi? Uzak bir sunucudaki dosyalar yerel sisteminizde nasıl görünüyor ve bu kadar hızlı senkronize ediliyor? Bir Wikipedia makalesini dosya sisteminizdeki bir dosyaymış gibi düzenlemenize olanak tanıyan WikipediaFS'yi duydunuz mu? Bu sihir değil; FUSE'nin (Kullanıcı Alanındaki Dosya Sistemi) gücüdür. FUSE, işletim sistemi çekirdeği veya düşük seviyeli programlama dilleri hakkında derin bilgiye ihtiyaç duymadan kendi dosya sisteminizi oluşturmanıza olanak tanır.
Bu makalede FUSE'ın Node.js ve TypeScript ile kullanılmasına yönelik pratik bir çözüm tanıtılmaktadır. FUSE'ın nasıl çalıştığını keşfedeceğiz ve gerçek dünyadaki bir görevi çözerek uygulamasını göstereceğiz. FUSE ve Node.js dünyasındaki heyecan verici macerada bana katılın.
İşimde medya dosyalarından (öncelikle görsellerden) sorumluydum. Buna pek çok şey dahildir: yan veya üst banner'lar, sohbetlerdeki medya, çıkartmalar vb. Elbette bunlar için "banner PNG veya WEBP, 300x1000 pikseldir" gibi birçok gereksinim vardır. Gereksinimlerin karşılanmaması durumunda arka ofisimiz görselin geçmesine izin vermeyecektir. Ve bir nesne tekilleştirme mekanizması mevcut: hiçbir görüntü aynı nehre iki kez giremez.
Bu bizi test amaçlı çok sayıda görselin olduğu bir duruma getiriyor. Hayatımı kolaylaştırmak için tek satırlık kabuklar veya takma adlar kullandım.
Örneğin:
convert -size 300x1000 xc:gray +noise random /tmp/out.png
bash
ve convert
birleşimi harika bir araçtır, ancak açıkçası bu, sorunu çözmenin en uygun yolu değildir. Kalite Güvence ekibinin durumunun tartışılması daha fazla komplikasyonu ortaya çıkarır. Görüntü oluşturmaya harcanan kayda değer sürenin yanı sıra, bir sorunu araştırdığımızda ilk soru "Benzersiz bir görüntü yüklediğinizden emin misiniz?" olur. Bunun ne kadar sinir bozucu olduğunu anladığınıza inanıyorum.
Basit bir yaklaşım benimseyebilirsiniz: GET /image/1000x100/random.zip?imagesCount=100
gibi, kendini açıklayan bir dosyaya sahip bir rota sunan bir web hizmeti oluşturun. Rota, bir dizi benzersiz resim içeren bir ZIP dosyası döndürecektir. Bu kulağa hoş geliyor ancak asıl sorunumuzu çözmüyor: yüklenen tüm dosyaların test için benzersiz olması gerekiyor.
Bir sonraki düşünceniz "Bir yükü gönderirken değiştirebilir miyiz?" olabilir. QA ekibi API çağrıları için Postman'ı kullanıyor. Postman'ın dahili bilgilerini araştırdım ve istek gövdesini "anında" değiştiremeyeceğimizi fark ettim.
Başka bir çözüm, bir şey dosyayı okumaya çalıştığında dosya sistemindeki dosyayı değiştirmektir. Linux'ta, dizinlerdeki değişiklikler veya dosya değişiklikleri gibi dosya sistemi olayları hakkında sizi uyaran, Inotify adında bir bildirim alt sistemi vardır. "Visual Studio Code bu büyük çalışma alanındaki dosya değişikliklerini izleyemiyor" mesajını alıyorsanız Inotify ile ilgili bir sorun var demektir. Bir dizin değiştirildiğinde, bir dosya yeniden adlandırıldığında, bir dosya açıldığında vb. bir olayı tetikleyebilir.
Etkinliklerin tam listesini burada bulabilirsiniz: https://sites.uclouvain.be/SystInfo/usr/include/linux/inotify.h.html
Yani plan şu:
IN_OPEN
olayını dinlemek ve dosya tanımlayıcılarını saymak.
IN_CLOSE
etkinliğini dinliyoruz; eğer sayı 0'a düşerse dosyayı değiştireceğiz.
Kulağa hoş geliyor ama bununla ilgili birkaç sorun var:
inotify
destekler.
Bu sorunları çözmek için kendi dosya sistemimizi yazabiliriz. Ancak başka bir sorun daha var: Normal dosya sistemi işletim sistemi çekirdek alanında çalışıyor. İşletim sistemi çekirdeğini bilmemizi ve C/Rust gibi dilleri kullanmamızı gerektirir. Ayrıca her çekirdek için özel bir modül (sürücü) yazmalıyız.
Bu nedenle, çözmek istediğimiz sorun için bir dosya sistemi yazmak gereksizdir; Önümüzde uzun bir hafta sonu olsa bile. Neyse ki bu canavarı evcilleştirmenin bir yolu var: Use rspace'deki dosya sistemi (FUSE). FUSE, çekirdek kodunu düzenlemeden dosya sistemleri oluşturmanıza olanak tanıyan bir projedir. Bu, FUSE aracılığıyla herhangi bir programın veya komut dosyasının, çekirdekle ilgili karmaşık bir mantık olmadan, bir flash, sabit sürücü veya SSD'yi taklit edebileceği anlamına gelir.
Başka bir deyişle, sıradan bir kullanıcı alanı süreci, Nautilus, Dolphin, ls, vb. gibi herhangi bir sıradan program aracılığıyla normal şekilde erişilebilen kendi dosya sistemini oluşturabilir.
FUSE neden gereksinimlerimizi karşılamak için iyidir? FUSE tabanlı dosya sistemleri kullanıcı aralıklı işlemler üzerine kurulmuştur. Bu nedenle, libfuse
ile bağlantısı olan bildiğiniz herhangi bir dili kullanabilirsiniz. Ayrıca FUSE ile platformlar arası bir çözüm elde edersiniz.
NodeJS ve TypeScript ile çok fazla deneyimim oldu ve bu (harika) kombinasyonu, yepyeni FS'miz için yürütme ortamı olarak seçmek istiyorum. Ayrıca TypeScript mükemmel bir nesne yönelimli temel sağlar. Bu size yalnızca herkese açık GitHub deposunda bulabileceğiniz kaynak kodunu değil aynı zamanda projenin yapısını da göstermemi sağlayacak.
Resmi FUSE sayfasından bir konuşma alıntısı yapayım:
FUSE bir kullanıcı alanı dosya sistemi çerçevesidir. Bir çekirdek modülü (fuse.ko), bir kullanıcı alanı kitaplığı (libfuse.*) ve bir bağlama yardımcı programından (fusermount) oluşur.
Dosya sistemleri yazmaya yönelik bir çerçeve heyecan verici geliyor.
Her bir FUSE parçasının ne anlama geldiğini açıklamalıyım:
fuse.ko
çekirdekle ilgili tüm düşük seviyeli işleri yapıyor; bu, bir işletim sistemi çekirdeğine müdahaleyi önlememizi sağlar.
libfuse
fuse.ko
ile iletişim için üst düzey bir katman sağlayan bir kütüphanedir.
fusermount
kullanıcıların kullanıcı alanı dosya sistemlerini bağlamasına/bağlantısını kaldırmasına olanak tanır (bana Kaptan Açık deyin!).
Genel prensipler şöyle görünür:
Kullanıcı alanı işlemi (bu durumda ls
), isteği FUSE çekirdek modülüne yönlendiren Sanal Dosya Sistemi çekirdeğine bir istekte bulunur. FUSE modülü ise isteği kullanıcı alanına, dosya sistemi gerçekleştirmesine (yukarıdaki resimde ./hello
) geri yönlendirir.
Sanal Dosya Sistemi adına aldanmayın. Doğrudan SİGORTA ile ilgili değildir. Kullanıcı alanı programlarına dosya sistemi arayüzünü sağlayan çekirdekteki yazılım katmanıdır. Basitlik adına bunu bir Kompozit desen olarak algılayabilirsiniz.
libfuse
iki tür API sunar: yüksek düzey ve düşük düzey. Benzerlikleri var ama önemli farklılıkları var. Düşük seviyeli olan eşzamansızdır ve yalnızca inodes
çalışır. Bu durumda eşzamansız, düşük düzeyli API kullanan bir istemcinin yanıt yöntemlerini kendisi çağırması gerektiği anlamına gelir.
Üst düzey olan, daha fazla "soyut" inodes
yerine uygun yolları (örneğin, /etc/shadow
) kullanma yeteneği sağlar ve yanıtları senkronize bir şekilde döndürür. Bu yazımda low-level ve inodes
ziyade high-level'ın nasıl çalıştığını anlatacağım.
Kendi dosya sisteminizi uygulamak istiyorsanız, VFS'den sunulan isteklerden sorumlu bir dizi yöntemi uygulamanız gerekir. En yaygın yöntemler şunlardır:
open(path, accessFlags): fd
-- bir dosyayı yola göre açar. Yöntem, Dosya Tanımlayıcı olarak adlandırılan (bundan böyle fd
) bir sayı tanımlayıcı döndürecektir. Erişim bayrakları, istemci programının hangi işlemi gerçekleştirmek istediğini (salt okunur, salt yazılır, okuma-yazma, yürütme veya arama) açıklayan ikili bir maskedir.
read(path, fd, Buffer, size, offset): count of bytes read
-- fd
Dosya Tanımlayıcısı ile bağlantılı bir dosyadan geçirilen Buffer'a size
baytlarını okuyun. path
argümanı göz ardı edilir çünkü fd'yi kullanacağız.
write(path, fd, Buffer, size, offset): count of bytes written
-- size
baytlarını Buffer'dan fd
ile bağlantılı bir dosyaya yazın.
release(fd)
- fd
kapatın.
truncate(path, size)
- dosya boyutunu değiştirin. Dosyaları yeniden yazmak istiyorsanız yöntem tanımlanmalıdır (ve biz yaparız).
getattr(path)
- boyut, oluşturulduğu yer, erişildiği yer vb. gibi dosya parametrelerini döndürür. Yöntem, dosya sistemi tarafından en çok çağrılabilir yöntemdir, bu nedenle en uygun olanı oluşturduğunuzdan emin olun.
readdir(path)
- tüm alt dizinleri oku.
Yukarıdaki yöntemler, üst düzey FUSE API üzerine kurulu, tamamen çalıştırılabilir her dosya sistemi için hayati öneme sahiptir. Ancak liste tam değil; tam listeyi https://libfuse.github.io/doxygen/structfuse__operations.html adresinde bulabilirsiniz.
Dosya tanımlayıcı kavramına tekrar dönecek olursak: MacOS dahil UNIX benzeri sistemlerde, dosya tanımlayıcı dosyalar ve yuvalar ve kanallar gibi diğer G/Ç kaynakları için bir soyutlamadır. Bir program bir dosyayı açtığında, işletim sistemi dosya tanımlayıcı adı verilen sayısal bir tanımlayıcı döndürür. Bu tam sayı, her işlem için işletim sisteminin dosya tanımlayıcı tablosunda bir dizin görevi görür. FUSE kullanarak bir dosya sistemi uygularken dosya tanımlayıcılarını kendimiz oluşturmamız gerekecektir.
İstemci bir dosyayı açtığında çağrı akışını ele alalım:
getattr(path: /random.png) → { size: 98 };
istemci dosya boyutunu aldı.
open(path: /random.png) → 10;
dosya yola göre açıldı; FUSE uygulaması bir dosya tanımlayıcı numarası döndürür.
read(path: /random.png, fd: 10 buffer, size: 50, offset: 0) → 50;
ilk 50 baytı okuyun.
read(path: /random.png, fd: 10 buffer, size: 50, offset: 50) → 48;
sonraki 50 baytı okuyun. Dosya boyutu nedeniyle 48 bayt okundu.
release(10);
tüm veriler okundu, fd'ye çok yakın.
Bir sonraki adımımız, Postman'ın özel bir dosya sistemiyle nasıl etkileşime gireceğini test etmek için libfuse
dayalı minimal bir dosya sistemi geliştirmektir.
FS için kabul gereksinimleri basittir: FS'nin kökü, içeriği her okunduğunda benzersiz olması gereken bir random.txt
dosyası içermelidir (buna "her zaman benzersiz okuma" diyelim). İçerik, yeni bir satırla ayrılmış olarak rastgele bir UUID ve ISO formatında geçerli saati içermelidir. Örneğin:
3790d212-7e47-403a-a695-4d680f21b81c 2012-12-12T04:30:30
Minimal ürün iki parçadan oluşacaktır. Birincisi, HTTP POST isteklerini kabul edecek ve terminale bir istek gövdesi yazdıracak basit bir web hizmetidir. Kod oldukça basittir ve zaman ayırmaya değmez, çünkü makale Express ile değil FUSE ile ilgilidir. İkinci kısım ise gereksinimleri karşılayan dosya sisteminin uygulanmasıdır. Yalnızca 83 satır kod içerir.
Kod için, libfuse
üst düzey API'sine bağlamalar sağlayan node-fuse-bindings kitaplığını kullanacağız.
Aşağıdaki kodu atlayabilirsiniz; Aşağıda bir kod özeti yazacağım.
const crypto = require('crypto'); const fuse = require('node-fuse-bindings'); // MOUNT_PATH is the path where our filesystem will be available. For Windows, this will be a path like 'D://' const MOUNT_PATH = process.env.MOUNT_PATH || './mnt'; function getRandomContent() { const txt = [crypto.randomUUID(), new Date().toISOString(), ''].join('\n'); return Buffer.from(txt); } function main() { // fdCounter is a simple counter that increments each time a file is opened // using this we can get the file content, which is unique for each opening let fdCounter = 0; // fd2ContentMap is a map that stores file content by fd const fd2ContentMap = new Map(); // Postman does not work reliably if we give it a file with size 0 or just the wrong size, // so we precompute the file size // it is guaranteed that the file size will always be the same within one run, so there will be no problems with this const randomTxtSize = getRandomContent().length; // fuse.mount is a function that mounts the filesystem fuse.mount( MOUNT_PATH, { readdir(path, cb) { console.log('readdir(%s)', path); if (path === '/') { return cb(0, ['random.txt']); } return cb(0, []); }, getattr(path, cb) { console.log('getattr(%s)', path); if (path === '/') { return cb(0, { // mtime is the file modification time mtime: new Date(), // atime is the file access time atime: new Date(), // ctime is the metadata or file content change time ctime: new Date(), size: 100, // mode is the file access flags // this is a mask that defines access rights to the file for different types of users // and the type of file itself mode: 16877, // file owners // in our case, it will be the owner of the current process uid: process.getuid(), gid: process.getgid(), }); } if (path === '/random.txt') { return cb(0, { mtime: new Date(), atime: new Date(), ctime: new Date(), size: randomTxtSize, mode: 33188, uid: process.getuid(), gid: process.getgid(), }); } cb(fuse.ENOENT); }, open(path, flags, cb) { console.log('open(%s, %d)', path, flags); if (path !== '/random.txt') return cb(fuse.ENOENT, 0); const fd = fdCounter++; fd2ContentMap.set(fd, getRandomContent()); cb(0, fd); }, read(path, fd, buf, len, pos, cb) { console.log('read(%s, %d, %d, %d)', path, fd, len, pos); const buffer = fd2ContentMap.get(fd); if (!buffer) { return cb(fuse.EBADF); } const slice = buffer.slice(pos, pos + len); slice.copy(buf); return cb(slice.length); }, release(path, fd, cb) { console.log('release(%s, %d)', path, fd); fd2ContentMap.delete(fd); cb(0); }, }, function (err) { if (err) throw err; console.log('filesystem mounted on ' + MOUNT_PATH); }, ); } // Handle the SIGINT signal separately to correctly unmount the filesystem // Without this, the filesystem will not be unmounted and will hang in the system // If for some reason unmount was not called, you can forcibly unmount the filesystem using the command // fusermount -u ./MOUNT_PATH process.on('SIGINT', function () { fuse.unmount(MOUNT_PATH, function () { console.log('filesystem at ' + MOUNT_PATH + ' unmounted'); process.exit(); }); }); main();
Bir dosyadaki izin bitleri hakkındaki bilgilerimizi yenilemenizi öneririm. İzin bitleri, bir dosyayla ilişkili bir bit kümesidir; bunlar dosyayı kimin okumasına/yazmasına/yürütmesine izin verildiğinin ikili temsilidir. "Kim" üç grubu içerir: sahip, sahip grubu ve diğerleri.
İzinler her grup için ayrı ayrı ayarlanabilir. Genellikle her izin üç haneli bir sayıyla temsil edilir: okuma (ikili sayı sisteminde 4 veya '100'), yazma (2 veya '010') ve yürütme (1 veya '001'). Bu numaraları bir araya getirirseniz birleşik bir izin oluşturacaksınız. Örneğin, 4 + 2 (veya '100' + '010') 6 ('110') eder, bu da okuma + yazma (RO) izni anlamına gelir.
Dosya sahibinin erişim maskesi 7 ise (ikili sistemde 111, okuma, yazma ve yürütme anlamına gelir), grupta 5 (101, okuma ve yürütme anlamına gelir) ve diğerlerinde 4 (100, salt okunur anlamına gelir) bulunur. Bu nedenle, dosyanın tam erişim maskesi ondalık sayı olarak 754'tür. Yürütme izninin, dizinler için okuma izni haline geldiğini unutmayın.
Dosya sistemi uygulamasına geri dönelim ve bunun metin versiyonunu yapalım: Bir dosya her açıldığında ( open
çağrı yoluyla), tamsayı sayacı artar ve açık çağrı tarafından döndürülen dosya tanımlayıcıyı üretir. Daha sonra rastgele içerik oluşturulur ve dosya tanımlayıcının anahtar olduğu bir anahtar-değer deposuna kaydedilir. Bir okuma çağrısı yapıldığında karşılık gelen içerik kısmı döndürülür.
Serbest bırakma çağrısı üzerine içerik silinir. Ctrl+C tuşlarına bastıktan sonra dosya sisteminin bağlantısını kesmek için SIGINT
kullanmayı unutmayın. Aksi takdirde, bunu terminalde fusermount -u ./MOUNT_PATH
kullanarak manuel olarak yapmamız gerekecek.
Şimdi teste geçin. Web sunucusunu çalıştırıyoruz, ardından gelecek FS için kök klasör olarak boş bir klasör oluşturup ana betiği çalıştırıyoruz. "3000 numaralı bağlantı noktasında sunucu dinleniyor" satırı yazdırıldıktan sonra, Postacı'yı açın ve herhangi bir parametreyi değiştirmeden web sunucusuna arka arkaya birkaç istek gönderin.
Her şey iyi gözüküyor! Her isteğin öngördüğümüz gibi benzersiz dosya içeriği vardır. Günlükler ayrıca yukarıda "SİGORTA'ya derinlemesine bakış" bölümünde açıklanan dosya açma çağrılarının akışının doğru olduğunu kanıtlar.
MVP'li GitHub deposu: https://github.com/pinkiesky/node-fuse-mvp . Bu kodu yerel ortamınızda çalıştırabilir veya bu repoyu kendi dosya sistemi uygulamanız için standart olarak kullanabilirsiniz.
Yaklaşım kontrol edildi; şimdi birincil uygulamanın zamanı geldi.
"Always benzersiz okuma" uygulamasından önce uygulamamız gereken ilk şey, orijinal dosyalar için oluşturma ve silme işlemlerini yapmaktır. Bu arayüzü sanal dosya sistemimizdeki bir dizin aracılığıyla uygulayacağız. Kullanıcı "her zaman benzersiz" veya "rastgele" olmasını istediği orijinal görselleri koyacak ve gerisini dosya sistemi hazırlayacaktır.
Burada ve sonraki bölümlerde, "her zaman benzersiz okuma", "rastgele görüntü" veya "rastgele dosya", her okunduğunda ikili anlamda benzersiz içerik döndüren ve görsel olarak mümkün olduğu kadar benzer kalan bir dosyayı ifade eder. orijinaline.
Dosya sisteminin kökü iki dizin içerecektir: Image Manager ve Images. Bunlardan ilki, kullanıcının orijinal dosyalarını yönetmeye yarayan bir klasördür (bunu bir CRUD deposu olarak düşünebilirsiniz). İkincisi ise rastgele görsellerin yer aldığı, kullanıcı açısından yönetilmeyen dizindir.
Yukarıdaki resimde görebileceğiniz gibi, yalnızca "her zaman benzersiz" görsellerin yanı sıra bir dosya dönüştürücü de uygulayacağız! Bu ek bir avantaj.
Uygulamamızın temel fikri, programın her düğüm ve yaprağın ortak FUSE yöntemleri sağladığı bir nesne ağacı içermesidir. Program bir FS çağrısı aldığında, karşılık gelen yolu kullanarak ağaçta bir düğüm veya yaprak bulmalıdır. Örneğin, program getattr(/Images/1/original/)
çağrısını alır ve ardından yolun adreslendiği düğümü bulmaya çalışır.
Bir sonraki soru orijinal görselleri nasıl saklayacağımızdır. Programdaki bir görüntü, ikili verilerden ve meta bilgilerinden oluşacaktır (bir meta, orijinal dosya adını, dosya mime türünü vb. içerir). İkili veriler ikili depolamada saklanacaktır. Bunu basitleştirelim ve kullanıcı (veya ana bilgisayar) dosya sisteminde bir dizi ikili dosya olarak ikili depolama oluşturalım. Meta bilgileri de benzer şekilde depolanacaktır: JSON, kullanıcı dosya sistemindeki metin dosyalarının içinde.
Hatırlayacağınız gibi "Minimum uygulanabilir bir ürün yazalım" bölümünde şablonla metin dosyası döndüren bir dosya sistemi oluşturmuştuk. Rastgele bir UUID ve geçerli bir tarih içerdiğinden, sorun verinin benzersizliği değildi; benzersizlik, verinin tanımıyla sağlandı. Ancak bu noktadan itibaren programın önceden yüklenmiş kullanıcı görselleri ile çalışması gerekmektedir. Peki, orijinaline dayalı olarak benzer ancak her zaman benzersiz (bayt ve dolayısıyla karma açısından) görüntüler nasıl oluşturabiliriz?
Önerdiğim çözüm oldukça basit. Görüntünün sol üst köşesine bir RGB gürültü karesi koyalım. Gürültü karesi 16x16 piksel olmalıdır. Bu hemen hemen aynı resmi sağlar ancak benzersiz bir bayt dizisini garanti eder. Çok sayıda farklı görüntü sağlamak yeterli olacak mı? Biraz matematik yapalım. Karenin boyutu 16. 16×16 = 256 RGB piksel tek karedir. Her pikselin 256×256×256 = 16.777.216 çeşidi vardır.
Dolayısıyla benzersiz karelerin sayısı 16.777.216^256'dır; 1.558 basamaklı bir sayıdır bu, gözlemlenebilir evrendeki atom sayısından çok daha fazladır. Bu kare boyutunu küçültebileceğimiz anlamına mı geliyor? Ne yazık ki, JPEG gibi kayıplı sıkıştırma, benzersiz karelerin sayısını önemli ölçüde azaltacaktır; dolayısıyla en uygun boyut 16x16'dır.
IFUSEHandler
yaygın FUSE çağrılarına hizmet eden bir arayüzdür. Sırasıyla read/write
readAll/writeAll
ile değiştirdiğimi görebilirsiniz. Bunu okuma ve yazma işlemlerini basitleştirmek için yaptım: IFUSEHandler
bir bölümün tamamını okuma/yazma yaptığında kısmi okuma/yazma mantığını başka bir yere taşıyabiliriz. Bu, IFUSEHandler
dosya tanımlayıcıları, ikili veriler vb. hakkında hiçbir şey bilmesine gerek olmadığı anlamına gelir.
open
FUSE yönteminde de aynı şey oldu. Ağacın dikkat çekici bir özelliği talep üzerine üretilmesidir. Program, ağacın tamamını belleğe depolamak yerine yalnızca erişildiğinde düğümler oluşturur. Bu davranış, programın, düğüm oluşturulması veya kaldırılması durumunda ağacın yeniden oluşturulmasıyla ilgili bir sorunu önlemesine olanak tanır.
ObjectTreeNode
arayüzünü kontrol ettiğinizde children
bir dizi değil, bir yöntem olduğunu göreceksiniz, yani talep üzerine bu şekilde oluşturulduklarını göreceksiniz. FileFUSETreeNode
ve DirectoryFUSETreeNode
bazı yöntemlerin NotSupported
hatası verdiği soyut sınıflardır (açıkçası, FileFUSETreeNode
hiçbir zaman readdir
uygulamamalıdır).
FUSEFacade programın ana mantığını uygulayan ve farklı parçaları birbirine bağlayan en önemli sınıftır. node-fuse-bindings
geri çağırma tabanlı bir API'ye sahiptir, ancak FUSEFacade yöntemleri Promise tabanlı bir API ile yapılmıştır. Bu rahatsızlığı gidermek için şöyle bir kod kullandım:
const handleResultWrapper = <T>( promise: Promise<T>, cb: (err: number, result: T) => void, ) => { promise .then((result) => { cb(0, result); }) .catch((err) => { if (err instanceof FUSEError) { fuseLogger.info(`FUSE error: ${err}`); return cb(err.code, null as T); } fuseLogger.warn(err); cb(fuse.EIO, null as T); }); }; // Ex. usage: // open(path, flags, cb) { // handleResultWrapper(fuseFacade.open(path, flags), cb); // },
FUSEFacade
yöntemleri handleResultWrapper
içine sarılmıştır. Bir yol kullanan her FUSEFacade
yöntemi, yolu ayrıştırır, ağaçta bir düğüm bulur ve istenen yöntemi çağırır.
FUSEFacade
sınıfından birkaç yöntemi düşünün.
async create(path: string, mode: number): Promise<number> { this.logger.info(`create(${path})`); // Convert path `/Image Manager/1/image.jpg` in // `['Image Manager', '1', 'image.jpg']` // splitPath will throw error if something goes wrong const parsedPath = this.splitPath(path); // `['Image Manager', '1', 'image.jpg']` const name = parsedPath.pop()!; // 'image.jpg' // Get node by path (`/Image Manager/1` after `pop` call) // or throw an error if node not found const node = await this.safeGetNode(parsedPath); // Call the IFUSEHandler method. Pass only a name, not a full path! await node.create(name, mode); // Create a file descriptor const fdObject = this.fdStorage.openWO(); return fdObject.fd; } async readdir(path: string): Promise<string[]> { this.logger.info(`readdir(${path})`); const node = await this.safeGetNode(path); // As you see, the tree is generated on the fly return (await node.children()).map((child) => child.name); } async open(path: string, flags: number): Promise<number> { this.logger.info(`open(${path}, ${flags})`); const node = await this.safeGetNode(path); // A leaf node is a directory if (!node.isLeaf) { throw new FUSEError(fuse.EACCES, 'invalid path'); } // Usually checkAvailability checks access await node.checkAvailability(flags); // Get node content and put it in created file descriptor const fileData: Buffer = await node.readAll(); // fdStorage is IFileDescriptorStorage, we will consider it below const fdObject = this.fdStorage.openRO(fileData); return fdObject.fd; }
Bir sonraki adıma geçmeden önce programımızın bağlamında dosya tanımlayıcının ne olduğuna daha yakından bakalım.
ReadWriteFileDescriptor
dosya tanımlayıcılarını sayı olarak ve ikili verileri arabellek olarak saklayan bir sınıftır. Sınıf, bir dosya tanımlayıcı arabelleğine veri okuma ve yazma yeteneği sağlayan readToBuffer
ve writeToBuffer
yöntemlerine sahiptir. ReadFileDescriptor
ve WriteFileDescriptor
salt okunur ve salt yazılır tanımlayıcıların uygulamalarıdır.
IFileDescriptorStorage
dosya tanımlayıcı depolamasını açıklayan bir arayüzdür. Programın bu arayüz için tek bir uygulaması var: InMemoryFileDescriptorStorage
. Adından da anlaşılacağı gibi dosya tanımlayıcılarını hafızada saklıyor çünkü tanımlayıcılar için kalıcılığa ihtiyacımız yok.
FUSEFacade
dosya tanımlayıcılarını ve depolamayı nasıl kullandığını kontrol edelim:
async read( fd: number, // File descriptor to read from buf: Buffer, // Buffer to store the read data len: number, // Length of data to read pos: number, // Position in the file to start reading from ): Promise<number> { // Retrieve the file descriptor object from storage const fdObject = this.fdStorage.get(fd); if (!fdObject) { // If the file descriptor is invalid, throw an error throw new FUSEError(fuse.EBADF, 'invalid fd'); } // Read data into the buffer and return the number of bytes read return fdObject.readToBuffer(buf, len, pos); } async write( fd: number, // File descriptor to write to buf: Buffer, // Buffer containing the data to write len: number, // Length of data to write pos: number, // Position in the file to start writing at ): Promise<number> { // Retrieve the file descriptor object from storage const fdObject = this.fdStorage.get(fd); if (!fdObject) { // If the file descriptor is invalid, throw an error throw new FUSEError(fuse.EBADF, 'invalid fd'); } // Write data from the buffer and return the number of bytes written return fdObject.writeToBuffer(buf, len, pos); } async release(path: string, fd: number): Promise<0> { // Retrieve the file descriptor object from storage const fdObject = this.fdStorage.get(fd); if (!fdObject) { // If the file descriptor is invalid, throw an error throw new FUSEError(fuse.EBADF, 'invalid fd'); } // Safely get the node corresponding to the file path const node = await this.safeGetNode(path); // Write all the data from the file descriptor object to the node await node.writeAll(fdObject.binary); // Release the file descriptor from storage this.fdStorage.release(fd); // Return 0 indicating success return 0; }
Yukarıdaki kod basittir. Dosya tanımlayıcılarından okuma, yazma ve serbest bırakma yöntemlerini tanımlayarak, işlemleri gerçekleştirmeden önce dosya tanımlayıcının geçerli olmasını sağlar. Yayınlama yöntemi aynı zamanda bir dosya tanımlayıcı nesnesinden dosya sistemi düğümüne veri yazar ve dosya tanımlayıcıyı serbest bırakır.
libfuse
ve ağaç etrafındaki kodla işimiz bitti. Görüntüyle ilgili koda dalmanın zamanı geldi.
ImageMeta
bir görüntü hakkındaki meta bilgileri saklayan bir nesnedir. IImageMetaStorage
meta için bir depolamayı tanımlayan bir arayüzdür. Programın arayüz için yalnızca bir uygulaması vardır: FSImageMetaStorage
sınıfı, tek bir JSON dosyasında depolanan görüntü meta verilerini yönetmek için IImageMetaStorage
arayüzünü uygular.
Meta verileri bellekte depolamak için bir önbellek kullanır ve gerektiğinde JSON dosyasından okuyarak önbelleğin doldurulmasını sağlar. Sınıf, görüntü meta verilerini oluşturmaya, almaya, listelemeye ve silmeye yönelik yöntemler sağlar ve güncellemeleri sürdürmek için değişiklikleri JSON dosyasına geri yazar. Önbellek, GÇ işlem sayısını azaltarak performansı artırır.
ImageBinary
açıkçası ikili görüntü verilerine sahip bir nesnedir. Image
arayüzü ImageMeta
ve ImageBinary
bileşimidir.
IBinaryStorage
ikili veri depolamaya yönelik bir arayüzdür. İkili depolamanın görüntülerle bağlantısı kaldırılmalıdır ve her türlü veriyi depolayabilir: görüntüler, video, JSON veya metin. Bu gerçek bizim için önemlidir ve nedenini göreceksiniz.
IImageGenerator
bir oluşturucuyu tanımlayan bir arayüzdür. Jeneratör programın önemli bir parçasıdır. Ham ikili verileri ve metayı alır ve buna dayalı bir görüntü oluşturur. Programın neden jeneratörlere ihtiyacı var? Program onlarsız çalışabilir mi?
Yapabilir, ancak jeneratörler uygulamaya esneklik katacaktır. Jeneratörler, kullanıcıların resimleri, metin verilerini ve genel olarak bir jeneratör yazdığınız herhangi bir veriyi yüklemelerine olanak tanır.
Akış şu şekildedir: ikili veriler depodan yüklenir (yukarıdaki resimde myfile.txt
) ve ardından ikili dosya bir oluşturucuya aktarılır. "Anında" bir görüntü oluşturur. Bunu bize daha uygun olan bir formattan diğerine dönüştürücü olarak algılayabilirsiniz.
Bir jeneratör örneğine göz atalım:
import { createCanvas } from 'canvas'; // Import createCanvas function from the canvas library to create and manipulate images const IMAGE_SIZE_RE = /(\d+)x(\d+)/; // Regular expression to extract width and height dimensions from a string export class TextImageGenerator implements IImageGenerator { // method to generate an image from text async generate(meta: ImageMeta, rawBuffer: Buffer): Promise<Image | null> { // Step 1: Verify the MIME type is text if (meta.originalFileType !== MimeType.TXT) { // If the file type is not text, return null indicating no image generation return null; } // Step 2: Determine the size of the image const imageSize = { width: 800, // Default width height: 600, // Default height }; // Extract dimensions from the name if present const imageSizeRaw = IMAGE_SIZE_RE.exec(meta.name); if (imageSizeRaw) { // Update the width and height based on extracted values, or keep defaults imageSize.width = Number(imageSizeRaw[1]) || imageSize.width; imageSize.height = Number(imageSizeRaw[2]) || imageSize.height; } // Step 3: Convert the raw buffer to a string to get the text content const imageText = rawBuffer.toString('utf-8'); // Step 4: Create a canvas with the determined size const canvas = createCanvas(imageSize.width, imageSize.height); const ctx = canvas.getContext('2d'); // Get the 2D drawing context // Step 5: Prepare the canvas background ctx.fillStyle = '#000000'; // Set fill color to black ctx.fillRect(0, 0, imageSize.width, imageSize.height); // Fill the entire canvas with the background color // Step 6: Draw the text onto the canvas ctx.textAlign = 'start'; // Align text to the start (left) ctx.textBaseline = 'top'; // Align text to the top ctx.fillStyle = '#ffffff'; // Set text color to white ctx.font = '30px Open Sans'; // Set font style and size ctx.fillText(imageText, 10, 10); // Draw the text with a margin // Step 7: Convert the canvas to a PNG buffer and create the Image object return { meta, // Include the original metadata binary: { buffer: canvas.toBuffer('image/png'), // Convert canvas content to a PNG buffer }, }; } }
ImageLoaderFacade
sınıfı, depolamayı ve jeneratörü mantıksal olarak birleştiren, yani yukarıda okuduğunuz akışı uygulayan bir cephedir .
IImageVariant
, çeşitli görüntü çeşitleri oluşturmaya yönelik bir arayüzdür. Bu bağlamda bir varyant, dosya sistemimizdeki dosyaları görüntülerken kullanıcıya görüntülenecek olan "anında" oluşturulan bir görüntüdür. Jeneratörlerden temel farkı girdi olarak ham veri yerine görüntü almasıdır.
Programın üç çeşidi vardır: ImageAlwaysRandom
, ImageOriginalVariant
ve ImageWithText
. ImageAlwaysRandom
orijinal görüntüyü rastgele bir RGB gürültü karesiyle döndürür.
export class ImageAlwaysRandomVariant implements IImageVariant { // Define a constant for the size of the random square edge in pixels private readonly randomSquareEdgeSizePx = 16; // Constructor takes the desired output format for the image constructor(private readonly outputFormat: ImageFormat) {} // Asynchronous method to generate a random variant of an image async generate(image: Image): Promise<ImageBinary> { // Step 1: Load the image using the sharp library const sharpImage = sharp(image.binary.buffer); // Step 2: Retrieve metadata and raw buffer from the image const metadata = await sharpImage.metadata(); // Get image metadata const buffer = await sharpImage.raw().toBuffer(); // Get raw pixel data // the buffer size is plain array with size of image width * image height * channels count (3 or 4) // Step 3: Apply random pixel values to a small square region in the image for (let y = 0; y < this.randomSquareEdgeSizePx; y++) { for (let x = 0; x < this.randomSquareEdgeSizePx; x++) { // Calculate the buffer offset for the current pixel const offset = y * metadata.width! * metadata.channels! + x * metadata.channels!; // Set random values for RGB channels buffer[offset + 0] = randInt(0, 255); // Red channel buffer[offset + 1] = randInt(0, 255); // Green channel buffer[offset + 2] = randInt(0, 255); // Blue channel // If the image has an alpha channel, set it to 255 (fully opaque) if (metadata.channels === 4) { buffer[offset + 3] = 255; // Alpha channel } } } // Step 4: Create a new sharp image from the modified buffer and convert it to the desired format const result = await sharp(buffer, { raw: { width: metadata.width!, height: metadata.height!, channels: metadata.channels!, }, }) .toFormat(this.outputFormat) // Convert to the specified output format .toBuffer(); // Get the final image buffer // Step 5: Return the generated image binary data return { buffer: result, // Buffer containing the generated image }; } }
sharp
kitaplığını NodeJS'deki görüntüler üzerinde çalışmanın en uygun yolu olarak kullanıyorum: https://github.com/lovell/sharp .
ImageOriginalVariant
herhangi bir değişiklik yapmadan bir görüntüyü döndürür (ancak bir görüntüyü farklı bir sıkıştırma biçiminde döndürebilir). ImageWithText
üzerinde yazılı metin bulunan bir resim döndürür. Bu, tek bir görüntünün önceden tanımlanmış çeşitlerini oluşturduğumuzda faydalı olacaktır. Örneğin, bir görüntünün 10 rastgele varyasyonuna ihtiyacımız varsa, bu varyasyonları birbirinden ayırmamız gerekir.
Buradaki çözüm, orijinali temel alan 10 resim oluşturmaktır; burada her görüntünün sol üst köşesinde 0'dan 9'a kadar sıralı bir sayı oluştururuz.
ImageCacheWrapper
varyantlardan farklı bir amacı vardır ve belirli IImageVariant
sınıfının sonuçlarını önbelleğe alarak sarmalayıcı görevi görür. Görüntü dönüştürücü, metinden görüntüye oluşturucular vb. gibi değişmeyen varlıkları sarmak için kullanılacaktır. Bu önbelleğe alma mekanizması, özellikle aynı görüntülerin birden çok kez okunması durumunda daha hızlı veri alımına olanak tanır.
Programın tüm ana bölümlerini ele aldık. Her şeyi bir arada birleştirmenin zamanı geldi.
Aşağıdaki sınıf diyagramı, ağaç sınıflarının görüntü karşılıklarıyla nasıl birleştirildiğini gösterir. Diyagram aşağıdan yukarıya doğru okunmalıdır. RootDir
(adlarda FUSETreeNode
son ekinden kaçınmama izin verin), programın uyguladığı dosya sisteminin kök dizinidir. Üst sıraya geçtiğinizde iki dizine bakın: ImagesDir
ve ImagesManagerDir
. ImagesManagerDir
kullanıcı resimleri listesini içerir ve bunların kontrol edilmesine olanak tanır. Daha sonra ImagesManagerItemFile
, belirli bir dosya için bir düğümdür. Bu sınıf CRUD işlemlerini uygular.
ImagesManagerDir'i bir düğümün olağan uygulaması olarak düşünün:
class ImageManagerDirFUSETreeNode extends DirectoryFUSETreeNode { name = 'Image Manager'; // Name of the directory constructor( private readonly imageMetaStorage: IImageMetaStorage, private readonly imageBinaryStorage: IBinaryStorage, ) { super(); // Call the parent class constructor } async children(): Promise<IFUSETreeNode[]> { // Dynamically create child nodes // In some cases, dynamic behavior can be problematic, requiring a cache of child nodes // to avoid redundant creation of IFUSETreeNode instances const list = await this.imageMetaStorage.list(); return list.map( (meta) => new ImageManagerItemFileFUSETreeNode( this.imageMetaStorage, this.imageBinaryStorage, meta, ), ); } async create(name: string, mode: number): Promise<void> { // Create a new image metadata entry await this.imageMetaStorage.create(name); } async getattr(): Promise<Stats> { return { // File modification date mtime: new Date(), // File last access date atime: new Date(), // File creation date // We do not store dates for our images, // so we simply return the current date ctime: new Date(), // Number of links nlink: 1, size: 100, // File access flags mode: FUSEMode.directory( FUSEMode.ALLOW_RWX, // Owner access rights FUSEMode.ALLOW_RX, // Group access rights FUSEMode.ALLOW_RX, // Access rights for all others ), // User ID of the file owner uid: process.getuid ? process.getuid() : 0, // Group ID for which the file is accessible gid: process.getgid ? process.getgid() : 0, }; } // Explicitly forbid deleting the 'Images Manager' folder remove(): Promise<void> { throw FUSEError.accessDenied(); } }
İleriye doğru, ImagesDir
kullanıcının görsellerinin adını taşıyan alt dizinleri içerir. ImagesItemDir
her dizinden sorumludur. Mevcut tüm varyantları içerir; Hatırlayacağınız gibi varyant sayısı üç. Her değişken, farklı formatlardaki (şu anda: jpeg, png ve webm) son görüntü dosyalarını içeren bir dizindir. ImagesItemOriginalDir
ve ImagesItemCounterDir
, oluşturulan tüm ImageVariantFile
örneklerini bir önbellekte sarar.
Kodlama CPU tükettiğinden orijinal görüntülerin sürekli olarak yeniden kodlanmasını önlemek için bu gereklidir. Diyagramın en üstünde ImageVariantFile
bulunur. Daha önce açıklanan IFUSEHandler
ve IImageVariant
uygulamasının ve bileşiminin en önemli mücevheridir. Tüm çabalarımızın üzerine inşa ettiğimiz dosya bu.
Son dosya sisteminin aynı dosyaya gelen paralel istekleri nasıl ele aldığını test edelim. Bunu yapmak için, dosya sistemindeki dosyaları okuyacak ve karmalarını hesaplayacak md5sum
yardımcı programını birden çok iş parçacığında çalıştıracağız. Daha sonra bu karmaları karşılaştıracağız. Her şey doğru çalışıyorsa karmaların farklı olması gerekir.
#!/bin/bash # Loop to run the md5sum command 5 times in parallel for i in {1..5} do echo "Run $i..." # `&` at the end of the command runs it in the background md5sum ./mnt/Images/2020-09-10_22-43/always_random/2020-09-10_22-43.png & done echo 'wait...' # Wait for all background processes to finish wait
Komut dosyasını çalıştırdım ve aşağıdaki çıktıyı kontrol ettim (netlik sağlamak için biraz temizledim):
Run 1... Run 2... Run 3... Run 4... Run 5... wait... bcdda97c480db74e14b8779a4e5c9d64 0954d3b204c849ab553f1f5106d576aa 564eeadfd8d0b3e204f018c6716c36e9 73a92c5ef27992498ee038b1f4cfb05e 77db129e37fdd51ef68d93416fec4f65
Harika! Tüm karmalar farklıdır; bu, dosya sisteminin her seferinde benzersiz bir görüntü döndürdüğü anlamına gelir!
Umarım bu makale size kendi FUSE uygulamanızı yazmanız için ilham vermiştir. Unutmayın, bu projenin kaynak kodunu burada bulabilirsiniz: https://github.com/pinkiesky/node-fuse-images .
Oluşturduğumuz dosya sistemi, FUSE ve Node.js ile çalışmanın temel ilkelerini gösterecek şekilde basitleştirilmiştir. Örneğin doğru tarihleri dikkate almaz. Geliştirilebilecek çok yer var. Kullanıcı GIF dosyalarından çerçeve çıkarma, video kod dönüştürme ve hatta çalışanlar aracılığıyla görevleri paralelleştirme gibi işlevler eklediğinizi hayal edin.
Ancak mükemmel iyinin düşmanıdır. Sahip olduklarınızla başlayın, çalışmasını sağlayın ve ardından tekrarlayın. Mutlu kodlama!