paint-brush
FUSE और Node.js के साथ एक गतिशील फ़ाइल सिस्टम कैसे बनाएं: एक व्यावहारिक दृष्टिकोणद्वारा@rglr
386 रीडिंग
386 रीडिंग

FUSE और Node.js के साथ एक गतिशील फ़ाइल सिस्टम कैसे बनाएं: एक व्यावहारिक दृष्टिकोण

द्वारा Aleksandr Zinin33m2024/06/13
Read on Terminal Reader

बहुत लंबा; पढ़ने के लिए

क्या आपने कभी सोचा है कि जब आप sshfs user@remote:~/ /mnt/remoteroot चलाते हैं तो क्या होता है? रिमोट सर्वर से फ़ाइलें आपके स्थानीय सिस्टम पर कैसे दिखाई देती हैं और इतनी तेज़ी से सिंक्रोनाइज़ होती हैं? क्या आपने WikipediaFS के बारे में सुना है, जो आपको विकिपीडिया लेख को इस तरह संपादित करने की अनुमति देता है जैसे कि वह आपके फ़ाइल सिस्टम में एक फ़ाइल हो? यह कोई जादू नहीं है - यह FUSE (यूजरस्पेस में फ़ाइल सिस्टम) की शक्ति है। FUSE आपको OS कर्नेल या निम्न-स्तरीय प्रोग्रामिंग भाषाओं के गहन ज्ञान की आवश्यकता के बिना अपना स्वयं का फ़ाइल सिस्टम बनाने देता है। यह लेख Node.js और TypeScript के साथ FUSE का उपयोग करके एक व्यावहारिक समाधान प्रस्तुत करता है। हम यह पता लगाएंगे कि FUSE कैसे काम करता है और वास्तविक दुनिया के कार्य को हल करके इसके अनुप्रयोग का प्रदर्शन करेंगे। FUSE और Node.js की दुनिया में एक रोमांचक साहसिक कार्य में मेरे साथ जुड़ें।
featured image - FUSE और Node.js के साथ एक गतिशील फ़ाइल सिस्टम कैसे बनाएं: एक व्यावहारिक दृष्टिकोण
Aleksandr Zinin HackerNoon profile picture

क्या आपने कभी सोचा है कि जब आप sshfs user@remote:~/ /mnt/remoteroot चलाते हैं तो क्या होता है? रिमोट सर्वर से फ़ाइलें आपके स्थानीय सिस्टम पर कैसे दिखाई देती हैं और इतनी तेज़ी से सिंक्रोनाइज़ होती हैं? क्या आपने WikipediaFS के बारे में सुना है, जो आपको विकिपीडिया लेख को इस तरह संपादित करने की अनुमति देता है जैसे कि वह आपके फ़ाइल सिस्टम में एक फ़ाइल हो? यह कोई जादू नहीं है - यह FUSE (यूजरस्पेस में फ़ाइल सिस्टम) की शक्ति है। FUSE आपको OS कर्नेल या निम्न-स्तरीय प्रोग्रामिंग भाषाओं के गहन ज्ञान की आवश्यकता के बिना अपना स्वयं का फ़ाइल सिस्टम बनाने देता है।


यह लेख Node.js और TypeScript के साथ FUSE का उपयोग करके एक व्यावहारिक समाधान प्रस्तुत करता है। हम यह पता लगाएंगे कि FUSE किस तरह काम करता है और वास्तविक दुनिया के कार्य को हल करके इसके अनुप्रयोग को प्रदर्शित करेंगे। FUSE और Node.js की दुनिया में एक रोमांचक साहसिक कार्य में मेरे साथ शामिल हों।

परिचय

मैं अपने काम में मीडिया फ़ाइलों (मुख्य रूप से छवियों) के लिए ज़िम्मेदार था। इसमें कई चीज़ें शामिल हैं: साइड- या टॉप-बैनर, चैट में मीडिया, स्टिकर, आदि। बेशक, इनके लिए बहुत सारी ज़रूरतें हैं, जैसे कि "बैनर PNG या WEBP, 300x1000 पिक्सल है।" अगर ज़रूरतें पूरी नहीं होती हैं, तो हमारा बैक ऑफ़िस किसी छवि को पास नहीं होने देगा। और एक ऑब्जेक्ट डीडुप्लीकेशन मैकेनिज़्म है: कोई भी छवि एक ही नदी में दो बार प्रवेश नहीं कर सकती।


इससे हम ऐसी स्थिति में पहुंच जाते हैं जहां हमारे पास परीक्षण के लिए छवियों का एक विशाल सेट होता है। मैंने अपना काम आसान बनाने के लिए शेल वन-लाइनर्स या उपनामों का इस्तेमाल किया।


उदाहरण के लिए:

 convert -size 300x1000 xc:gray +noise random /tmp/out.png


शोर छवि का उदाहरण


bash और convert का संयोजन एक बेहतरीन टूल है, लेकिन जाहिर है, यह समस्या को हल करने का सबसे सुविधाजनक तरीका नहीं है। क्यूए टीम की स्थिति पर चर्चा करने से और भी जटिलताएँ सामने आती हैं। छवि निर्माण पर खर्च किए गए सराहनीय समय के अलावा, जब हम किसी समस्या की जाँच करते हैं तो पहला सवाल यह होता है कि "क्या आप वाकई एक अनूठी छवि अपलोड कर रहे हैं?" मुझे लगता है कि आप समझते हैं कि यह कितना परेशान करने वाला है।

आइए वह तकनीक चुनें जिसका हम उपयोग करना चाहते हैं

आप एक सरल तरीका अपना सकते हैं: एक वेब सेवा बनाएं जो एक स्व-व्याख्यात्मक फ़ाइल के साथ एक रूट प्रदान करती है, जैसे GET /image/1000x100/random.zip?imagesCount=100 । रूट अद्वितीय छवियों के एक सेट के साथ एक ज़िप फ़ाइल लौटाएगा। यह अच्छा लगता है, लेकिन यह हमारे मुख्य मुद्दे को संबोधित नहीं करता है: सभी अपलोड की गई फ़ाइलों को परीक्षण के लिए अद्वितीय होना चाहिए।


आपका अगला विचार यह हो सकता है कि "क्या हम पेलोड भेजते समय उसे बदल सकते हैं?" QA टीम API कॉल के लिए पोस्टमैन का उपयोग करती है। मैंने पोस्टमैन के आंतरिक भागों की जांच की और पाया कि हम अनुरोध बॉडी को "तुरंत" नहीं बदल सकते हैं।


दूसरा समाधान यह है कि जब भी कोई फ़ाइल पढ़ने की कोशिश करे, फ़ाइल सिस्टम में फ़ाइल को बदल दिया जाए। Linux में Inotify नामक एक अधिसूचना सबसिस्टम है, जो आपको फ़ाइल सिस्टम ईवेंट जैसे कि निर्देशिकाओं में परिवर्तन या फ़ाइल संशोधनों के बारे में सचेत करता है। यदि आपको "Visual Studio Code इस बड़े कार्यक्षेत्र में फ़ाइल परिवर्तनों को देखने में असमर्थ है" मिल रहा है, तो Inotify में कोई समस्या है। यह किसी निर्देशिका को बदलने, फ़ाइल का नाम बदलने, फ़ाइल को खोलने आदि पर ईवेंट फायर कर सकता है।


कार्यक्रमों की पूरी सूची यहां देखी जा सकती है: https://sites.uclouvain.be/SystInfo/usr/include/linux/inotify.h.html


तो, योजना यह है:

  1. IN_OPEN इवेंट को सुनना और फ़ाइल डिस्क्रिप्टर की गणना करना।

  2. IN_CLOSE इवेंट को सुनना; यदि गिनती 0 तक गिर जाती है, तो हम फ़ाइल को बदल देंगे।


सुनने में तो अच्छा लगता है, लेकिन इसमें कुछ समस्याएं हैं:

  • केवल लिनक्स inotify समर्थन करता है.
  • फ़ाइल के समानांतर अनुरोधों को समान डेटा वापस करना चाहिए।
  • यदि किसी फ़ाइल में गहन IO-संचालन है, तो प्रतिस्थापन कभी नहीं होगा।
  • यदि Inotify ईवेंट प्रदान करने वाली कोई सेवा क्रैश हो जाती है, तो फ़ाइलें उपयोगकर्ता फ़ाइल सिस्टम में ही रहेंगी।


इन समस्याओं को हल करने के लिए, हम अपना खुद का फ़ाइल सिस्टम लिख सकते हैं। लेकिन एक और समस्या है: नियमित फ़ाइल सिस्टम OS कर्नेल स्पेस में चलता है। इसके लिए हमें OS कर्नेल के बारे में जानना होगा और C/Rust जैसी भाषाओं का उपयोग करना होगा। साथ ही, प्रत्येक कर्नेल के लिए, हमें एक विशिष्ट मॉड्यूल (ड्राइवर) लिखना चाहिए।


इसलिए, जिस समस्या को हम हल करना चाहते हैं, उसके लिए फ़ाइल सिस्टम लिखना बहुत ज़्यादा है; भले ही आगे लंबा वीकेंड हो। सौभाग्य से, इस जानवर को वश में करने का एक तरीका है: फ़ाइल सिस्टम इन यूज़ आरस्पेस (FUSE)। FUSE एक ऐसा प्रोजेक्ट है जो आपको कर्नेल कोड को संपादित किए बिना फ़ाइल सिस्टम बनाने देता है। इसका मतलब है कि FUSE के ज़रिए कोई भी प्रोग्राम या स्क्रिप्ट, बिना किसी जटिल कोर-संबंधित तर्क के, फ़्लैश, हार्ड ड्राइव या SSD का अनुकरण करने में सक्षम है।


दूसरे शब्दों में, एक साधारण यूजरस्पेस प्रक्रिया अपना स्वयं का फ़ाइल सिस्टम बना सकती है, जिसे किसी भी साधारण प्रोग्राम के माध्यम से सामान्य रूप से एक्सेस किया जा सकता है - नॉटिलस, डॉल्फिन, एलएस, आदि।


FUSE हमारी आवश्यकताओं को पूरा करने के लिए क्यों अच्छा है? FUSE-आधारित फ़ाइल सिस्टम उपयोगकर्ता-स्पेस्ड प्रक्रियाओं पर बनाए गए हैं। इसलिए, आप किसी भी ऐसी भाषा का उपयोग कर सकते हैं जिसे आप जानते हैं और जिसका libfuse से संबंध है। साथ ही, आपको FUSE के साथ क्रॉस-प्लेटफ़ॉर्म समाधान मिलता है।


मुझे NodeJS और TypeScript का बहुत अनुभव है, और मैं अपने नए FS के लिए निष्पादन वातावरण के रूप में इस (अद्भुत) संयोजन को चुनना चाहूंगा। इसके अलावा, TypeScript एक बेहतरीन ऑब्जेक्ट-ओरिएंटेड बेस प्रदान करता है। इससे मैं आपको न केवल स्रोत कोड दिखा पाऊंगा, जिसे आप सार्वजनिक GitHub रेपो पर पा सकते हैं, बल्कि प्रोजेक्ट की संरचना भी दिखा पाऊंगा।

FUSE में गहराई से गोता लगाएँ

मैं आधिकारिक FUSE पृष्ठ से एक उद्धरण प्रस्तुत कर रहा हूँ:

FUSE एक यूजरस्पेस फाइलसिस्टम फ्रेमवर्क है। इसमें एक कर्नेल मॉड्यूल (fuse.ko), एक यूजरस्पेस लाइब्रेरी (libfuse.*) और एक माउंट यूटिलिटी (fusermount) शामिल है।


फ़ाइल सिस्टम लिखने के लिए एक फ्रेमवर्क रोमांचक लगता है।


मुझे यह बताना चाहिए कि प्रत्येक FUSE भाग का क्या अर्थ है:

  1. fuse.ko सभी कर्नेल-संबंधी निम्न-स्तरीय कार्य कर रहा है; इससे हमें OS कर्नेल में हस्तक्षेप से बचने में मदद मिलती है।


  2. libfuse एक लाइब्रेरी है जो fuse.ko के साथ संचार के लिए एक उच्च-स्तरीय परत प्रदान करती है।


  3. fusermount उपयोगकर्ताओं को यूजरस्पेस फ़ाइल सिस्टम को माउंट/अनमाउंट करने की अनुमति देता है (मुझे कैप्टन ऑब्विअस कहें!)।


सामान्य सिद्धांत इस प्रकार हैं:
FUSE के सामान्य सिद्धांत


यूजरस्पेस प्रक्रिया (इस मामले में ls ) वर्चुअल फ़ाइल सिस्टम कर्नेल से अनुरोध करती है जो अनुरोध को FUSE कर्नेल मॉड्यूल तक रूट करता है। बदले में, FUSE मॉड्यूल अनुरोध को वापस यूजरस्पेस से फ़ाइल सिस्टम कार्यान्वयन (ऊपर चित्र में ./hello ) तक रूट करता है।


वर्चुअल फ़ाइल सिस्टम नाम से धोखा न खाएं। यह सीधे FUSE से संबंधित नहीं है। यह कर्नेल में सॉफ़्टवेयर परत है जो उपयोगकर्ता स्थान प्रोग्राम को फ़ाइल सिस्टम इंटरफ़ेस प्रदान करती है। सरलता के लिए, आप इसे एक समग्र पैटर्न के रूप में समझ सकते हैं।


libfuse दो प्रकार के API प्रदान करता है: उच्च-स्तरीय और निम्न-स्तरीय। उनमें समानताएँ हैं लेकिन महत्वपूर्ण अंतर हैं। निम्न-स्तरीय एक एसिंक्रोनस है और केवल inodes के साथ काम करता है। इस मामले में, एसिंक्रोनस का मतलब है कि एक क्लाइंट जो निम्न-स्तरीय API का उपयोग करता है, उसे प्रतिक्रिया विधियों को स्वयं कॉल करना चाहिए।


उच्च-स्तरीय एक अधिक "अमूर्त" inodes के बजाय सुविधाजनक पथ (उदाहरण के लिए, /etc/shadow ) का उपयोग करने की क्षमता प्रदान करता है और सिंक तरीके से प्रतिक्रियाएँ लौटाता है। इस लेख में, मैं समझाऊँगा कि निम्न-स्तर और inodes के बजाय उच्च-स्तर कैसे काम करता है।


यदि आप अपना स्वयं का फ़ाइल सिस्टम लागू करना चाहते हैं, तो आपको VFS से अनुरोधों की सेवा के लिए जिम्मेदार विधियों का एक सेट लागू करना चाहिए। सबसे आम विधियाँ हैं:


  • open(path, accessFlags): fd -- पथ द्वारा फ़ाइल खोलें। विधि एक संख्या पहचानकर्ता, तथाकथित फ़ाइल डिस्क्रिप्टर (यहाँ से fd ) लौटाएगी। एक एक्सेस फ़्लैग एक बाइनरी मास्क है जो बताता है कि क्लाइंट प्रोग्राम कौन सा ऑपरेशन करना चाहता है (केवल पढ़ने के लिए, केवल लिखने के लिए, पढ़ने-लिखने के लिए, निष्पादित करने के लिए, या खोज करने के लिए)।


  • read(path, fd, Buffer, size, offset): count of bytes read -- fd फ़ाइल डिस्क्रिप्टर से लिंक की गई फ़ाइल से पास किए गए Buffer तक size bytes पढ़ें। path तर्क को अनदेखा किया जाता है क्योंकि हम fd का उपयोग करेंगे।


  • write(path, fd, Buffer, size, offset): count of bytes written - Buffer से fd से लिंक की गई फ़ाइल में size बाइट्स लिखें।


  • release(fd) -- fd बंद करें।


  • truncate(path, size) -- फ़ाइल का आकार बदलें। यदि आप फ़ाइलों को फिर से लिखना चाहते हैं (और हम ऐसा करते हैं) तो विधि को परिभाषित किया जाना चाहिए।


  • getattr(path) -- फ़ाइल पैरामीटर लौटाता है, जैसे आकार, निर्माण समय, पहुँच समय, आदि। यह विधि फ़ाइल सिस्टम द्वारा सबसे अधिक कॉल करने योग्य विधि है, इसलिए सुनिश्चित करें कि आप इष्टतम विधि बनाएं।


  • readdir(path) -- सभी उपनिर्देशिकाएँ पढ़ें.


ऊपर बताए गए तरीके उच्च-स्तरीय FUSE API के शीर्ष पर निर्मित प्रत्येक पूर्णतः संचालन योग्य फ़ाइल सिस्टम के लिए महत्वपूर्ण हैं। लेकिन सूची पूरी नहीं है; पूरी सूची आप https://libfuse.github.io/doxygen/structfuse__operations.html पर पा सकते हैं


फ़ाइल डिस्क्रिप्टर की अवधारणा पर फिर से विचार करें: UNIX-जैसे सिस्टम में, जिसमें MacOS भी शामिल है, फ़ाइल डिस्क्रिप्टर फ़ाइलों और सॉकेट और पाइप जैसे अन्य I/O संसाधनों के लिए एक अमूर्तता है। जब कोई प्रोग्राम फ़ाइल खोलता है, तो OS एक संख्यात्मक पहचानकर्ता लौटाता है जिसे फ़ाइल डिस्क्रिप्टर कहा जाता है। यह पूर्णांक प्रत्येक प्रक्रिया के लिए OS की फ़ाइल डिस्क्रिप्टर तालिका में एक सूचकांक के रूप में कार्य करता है। FUSE का उपयोग करके फ़ाइल सिस्टम को लागू करते समय, हमें फ़ाइल डिस्क्रिप्टर स्वयं बनाने की आवश्यकता होगी।


आइए, जब क्लाइंट कोई फ़ाइल खोलता है तो कॉल प्रवाह पर विचार करें:

  1. getattr(path: /random.png) → { size: 98 }; क्लाइंट को फ़ाइल का आकार मिल गया।


  2. open(path: /random.png) → 10; पथ द्वारा खोली गई फ़ाइल; FUSE कार्यान्वयन एक फ़ाइल डिस्क्रिप्टर संख्या लौटाता है।


  3. read(path: /random.png, fd: 10 buffer, size: 50, offset: 0) → 50; पहले 50 बाइट्स पढ़ें।


  4. read(path: /random.png, fd: 10 buffer, size: 50, offset: 50) → 48; अगले 50 को पढ़ें। फ़ाइल आकार के कारण 48 बाइट्स पढ़े गए थे।


  5. release(10); सभी डेटा पढ़ा गया था, इसलिए एफडी के करीब।

आइए एक न्यूनतम-व्यवहार्य उत्पाद लिखें और उस पर पोस्टमैन की प्रतिक्रिया देखें

हमारा अगला कदम libfuse पर आधारित एक न्यूनतम फाइल सिस्टम विकसित करना है, ताकि यह परीक्षण किया जा सके कि पोस्टमैन एक कस्टम फाइल सिस्टम के साथ किस प्रकार इंटरैक्ट करेगा।


FS के लिए स्वीकृति की आवश्यकताएं सीधी हैं: FS के मूल में एक random.txt फ़ाइल होनी चाहिए, जिसकी सामग्री हर बार पढ़ने पर अद्वितीय होनी चाहिए (चलिए इसे "हमेशा अद्वितीय पढ़ना" कहते हैं)। सामग्री में एक यादृच्छिक UUID और ISO प्रारूप में एक वर्तमान समय होना चाहिए, जिसे एक नई पंक्ति द्वारा अलग किया गया हो। उदाहरण के लिए:

 3790d212-7e47-403a-a695-4d680f21b81c 2012-12-12T04:30:30


न्यूनतम उत्पाद में दो भाग होंगे। पहला एक सरल वेब सेवा है जो HTTP POST अनुरोधों को स्वीकार करेगी और टर्मिनल पर अनुरोध निकाय प्रिंट करेगी। कोड काफी सरल है और हमारे समय के लायक नहीं है, मुख्यतः क्योंकि लेख FUSE के बारे में है, एक्सप्रेस के बारे में नहीं। दूसरा भाग फ़ाइल सिस्टम का कार्यान्वयन है जो आवश्यकताओं को पूरा करता है। इसमें कोड की केवल 83 पंक्तियाँ हैं।


कोड के लिए, हम node-fuse-bindings लाइब्रेरी का उपयोग करेंगे, जो libfuse के उच्च-स्तरीय API को बाइंडिंग प्रदान करती है।


आप नीचे दिए गए कोड को छोड़ सकते हैं; मैं नीचे कोड सारांश लिखने जा रहा हूँ।

 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();


मैं सुझाव देता हूँ कि फ़ाइल में अनुमति बिट्स के बारे में अपने ज्ञान को ताज़ा करें। अनुमति बिट्स बिट्स का एक सेट है जो फ़ाइल से जुड़े होते हैं; वे एक बाइनरी प्रतिनिधित्व हैं कि फ़ाइल को पढ़ने/लिखने/निष्पादित करने की अनुमति किसे है। "कौन" में तीन समूह शामिल हैं: स्वामी, स्वामी समूह, और अन्य।


प्रत्येक समूह के लिए अनुमतियाँ अलग-अलग सेट की जा सकती हैं। आमतौर पर, प्रत्येक अनुमति को तीन अंकों की संख्या द्वारा दर्शाया जाता है: पढ़ना (बाइनरी नंबर सिस्टम में 4 या '100'), लिखना (2 या '010'), और निष्पादन (1 या '001')। यदि आप इन संख्याओं को एक साथ जोड़ते हैं, तो आप एक संयुक्त अनुमति बनाएंगे। उदाहरण के लिए, 4 + 2 (या '100' + '010') 6 ('110') बनाएगा, जिसका अर्थ है पढ़ना + लिखना (आरओ) अनुमति।


यदि फ़ाइल स्वामी के पास 7 (बाइनरी में 111, जिसका अर्थ है पढ़ना, लिखना और निष्पादित करना) का एक्सेस मास्क है, तो समूह के पास 5 (101, जिसका अर्थ है पढ़ना और निष्पादित करना) है, और अन्य के पास 4 (100, जिसका अर्थ है केवल पढ़ने के लिए) है। इसलिए, फ़ाइल के लिए पूरा एक्सेस मास्क दशमलव में 754 है। ध्यान रखें कि निष्पादन अनुमति निर्देशिकाओं के लिए पढ़ने की अनुमति बन जाती है।


आइए फ़ाइल सिस्टम कार्यान्वयन पर वापस जाएं और इसका एक टेक्स्ट संस्करण बनाएं: हर बार जब कोई फ़ाइल खोली जाती है ( open कॉल के माध्यम से), पूर्णांक काउंटर बढ़ता है, जिससे ओपन कॉल द्वारा लौटाए गए फ़ाइल डिस्क्रिप्टर का निर्माण होता है। फिर यादृच्छिक सामग्री बनाई जाती है और फ़ाइल डिस्क्रिप्टर को कुंजी के रूप में कुंजी-मूल्य स्टोर में सहेजा जाता है। जब रीड कॉल किया जाता है, तो संबंधित सामग्री भाग लौटाया जाता है।


रिलीज़ कॉल पर, सामग्री हटा दी जाती है। Ctrl+C दबाने के बाद फ़ाइल सिस्टम को अनमाउंट करने के लिए SIGINT संभालना याद रखें। अन्यथा, हमें इसे टर्मिनल में fusermount -u ./MOUNT_PATH उपयोग करके मैन्युअल रूप से करना होगा।


अब, परीक्षण में कूदें। हम वेब सर्वर चलाते हैं, फिर आगामी FS के लिए रूट फ़ोल्डर के रूप में एक खाली फ़ोल्डर बनाते हैं, और मुख्य स्क्रिप्ट चलाते हैं। "सर्वर पोर्ट 3000 पर सुन रहा है" लाइन प्रिंट होने के बाद, पोस्टमैन खोलें, और किसी भी पैरामीटर को बदले बिना वेब-सर्वर को एक पंक्ति में कुछ अनुरोध भेजें।
बायीं ओर FS है, दायीं ओर वेब-सर्वर है


सब कुछ ठीक लग रहा है! जैसा कि हमने पहले ही अनुमान लगा लिया था, प्रत्येक अनुरोध में अद्वितीय फ़ाइल सामग्री होती है। लॉग यह भी साबित करते हैं कि "डीप डाइव इनटू FUSE" अनुभाग में ऊपर वर्णित फ़ाइल ओपन कॉल का प्रवाह सही है।


MVP के साथ GitHub रेपो: https://github.com/pinkiesky/node-fuse-mvp । आप इस कोड को अपने स्थानीय वातावरण पर चला सकते हैं या अपने स्वयं के फ़ाइल सिस्टम कार्यान्वयन के लिए बॉयलरप्लेट के रूप में इस रेपो का उपयोग कर सकते हैं।

मूल विचार

दृष्टिकोण की जाँच हो चुकी है - अब प्राथमिक कार्यान्वयन का समय है।


"हमेशा अद्वितीय रीड" कार्यान्वयन से पहले, पहली चीज़ जो हमें लागू करनी चाहिए वह है मूल फ़ाइलों के लिए क्रिएट और डिलीट ऑपरेशन। हम इस इंटरफ़ेस को अपने वर्चुअल फ़ाइल सिस्टम के भीतर एक निर्देशिका के माध्यम से लागू करेंगे। उपयोगकर्ता उन मूल छवियों को डालेंगे जिन्हें वे "हमेशा अद्वितीय" या "यादृच्छिक" बनाना चाहते हैं, और फ़ाइल सिस्टम बाकी को तैयार करेगा।


यहां और आगे के अनुभागों में, "हमेशा अद्वितीय पठन", "यादृच्छिक छवि" या "यादृच्छिक फ़ाइल" से तात्पर्य उस फ़ाइल से है जो प्रत्येक बार पढ़े जाने पर बाइनरी अर्थ में अद्वितीय सामग्री लौटाती है, जबकि दृश्य रूप से, यह मूल के यथासंभव समान रहती है।


फ़ाइल सिस्टम के रूट में दो निर्देशिकाएँ होंगी: इमेज मैनेजर और इमेजेस। पहला उपयोगकर्ता की मूल फ़ाइलों को प्रबंधित करने के लिए एक फ़ोल्डर है (आप इसे CRUD रिपॉजिटरी के रूप में सोच सकते हैं)। दूसरा उपयोगकर्ता के दृष्टिकोण से अप्रबंधित निर्देशिका है जिसमें यादृच्छिक छवियाँ होती हैं।
उपयोगकर्ता फ़ाइल सिस्टम के साथ इंटरैक्ट करता है


टर्मिनल आउटपुट के रूप में FS ट्री


जैसा कि आप ऊपर की छवि में देख सकते हैं, हम न केवल "हमेशा अद्वितीय" छवियां बल्कि एक फ़ाइल कनवर्टर भी लागू करेंगे! यह एक अतिरिक्त बोनस है।


हमारे कार्यान्वयन का मुख्य विचार यह है कि कार्यक्रम में एक ऑब्जेक्ट ट्री होगा, जिसमें प्रत्येक नोड और लीफ सामान्य FUSE विधियाँ प्रदान करेगा। जब प्रोग्राम को FS कॉल प्राप्त होता है, तो उसे संबंधित पथ द्वारा ट्री में एक नोड या लीफ ढूँढ़ना चाहिए। उदाहरण के लिए, प्रोग्राम getattr(/Images/1/original/) कॉल प्राप्त करता है और फिर उस नोड को खोजने का प्रयास करता है जिस पर पथ संबोधित है।


कुछ इस तरह: एफएस वृक्ष उदाहरण


अगला सवाल यह है कि हम मूल छवियों को कैसे संग्रहीत करेंगे। कार्यक्रम में एक छवि बाइनरी डेटा और मेटा जानकारी से मिलकर बनेगी (मेटा में एक मूल फ़ाइल नाम, फ़ाइल माइम-प्रकार, आदि शामिल हैं)। बाइनरी डेटा बाइनरी स्टोरेज में संग्रहीत किया जाएगा। आइए इसे सरल बनाएं और उपयोगकर्ता (या होस्ट) फ़ाइल सिस्टम में बाइनरी फ़ाइलों के एक सेट के रूप में बाइनरी स्टोरेज बनाएं। मेटा जानकारी इसी तरह संग्रहीत की जाएगी: उपयोगकर्ता फ़ाइल सिस्टम में टेक्स्ट फ़ाइलों के अंदर JSON।


जैसा कि आपको याद होगा, "आइए एक न्यूनतम-व्यवहार्य उत्पाद लिखें" अनुभाग में, हमने एक फ़ाइल सिस्टम बनाया जो एक टेम्पलेट द्वारा एक टेक्स्ट फ़ाइल लौटाता है। इसमें एक यादृच्छिक UUID और एक वर्तमान तिथि शामिल है, इसलिए डेटा की विशिष्टता समस्या नहीं थी - डेटा की परिभाषा द्वारा विशिष्टता प्राप्त की गई थी। हालाँकि, इस बिंदु से, प्रोग्राम को पहले से लोड किए गए उपयोगकर्ता छवियों के साथ काम करना चाहिए। तो, हम ऐसी छवियाँ कैसे बना सकते हैं जो मूल छवि के आधार पर समान लेकिन हमेशा अद्वितीय (बाइट्स और परिणामस्वरूप हैश के संदर्भ में) हों?


मैं जो समाधान सुझाता हूँ वह काफी सरल है। आइए एक छवि के ऊपरी-बाएँ कोने में एक RGB शोर वर्ग रखें। शोर वर्ग 16x16 पिक्सेल होना चाहिए। यह लगभग एक ही तस्वीर प्रदान करता है लेकिन बाइट्स के एक अद्वितीय अनुक्रम की गारंटी देता है। क्या यह बहुत सारी अलग-अलग छवियों को सुनिश्चित करने के लिए पर्याप्त होगा? आइए थोड़ा गणित करें। वर्ग का आकार 16 है। 16×16 = एक वर्ग में 256 RGB पिक्सेल। प्रत्येक पिक्सेल में 256×256×256 = 16,777,216 वैरिएंट होते हैं।


इस प्रकार, अद्वितीय वर्गों की संख्या 16,777,216^256 है - 1,558 अंकों वाली एक संख्या, जो अवलोकनीय ब्रह्मांड में परमाणुओं की संख्या से बहुत अधिक है। क्या इसका मतलब यह है कि हम वर्ग के आकार को कम कर सकते हैं? दुर्भाग्य से, JPEG जैसे हानिपूर्ण संपीड़न से अद्वितीय वर्गों की संख्या में काफी कमी आएगी, इसलिए 16x16 इष्टतम आकार है।


शोर वर्गों वाली छवियों का उदाहरण

कक्षाओं से आगे बढ़ें

पेड़
UML वर्ग आरेख FUSE-आधारित सिस्टम के लिए इंटरफेस और क्लास दिखाता है। इसमें IFUSEHandler, ObjectTreeNode और IFUSETreeNode इंटरफेस शामिल हैं, साथ ही FileFUSETreeNode और DirectoryFUSETreeNode IFUSETreeNode को लागू करते हैं। प्रत्येक इंटरफ़ेस और क्लास विशेषताओं और विधियों को सूचीबद्ध करता है, उनके संबंधों और पदानुक्रम को दर्शाता है

IFUSEHandler एक इंटरफ़ेस है जो सामान्य FUSE कॉल की सेवा करता है। आप देख सकते हैं कि मैंने read/write क्रमशः readAll/writeAll से बदल दिया है। मैंने ऐसा read और write संचालन को सरल बनाने के लिए किया: जब IFUSEHandler पूरे भाग के लिए read/write बनाता है, तो हम आंशिक read/write तर्क को दूसरे स्थान पर ले जाने में सक्षम होते हैं। इसका मतलब है कि IFUSEHandler फ़ाइल डिस्क्रिप्टर, बाइनरी डेटा आदि के बारे में कुछ भी जानने की आवश्यकता नहीं है।


open FUSE विधि के साथ भी यही हुआ। पेड़ का एक उल्लेखनीय पहलू यह है कि इसे मांग पर बनाया जाता है। पूरे पेड़ को मेमोरी में संग्रहीत करने के बजाय, प्रोग्राम केवल तभी नोड्स बनाता है जब उन्हें एक्सेस किया जाता है। यह व्यवहार प्रोग्राम को नोड निर्माण या हटाने के मामले में पेड़ के पुनर्निर्माण के साथ समस्या से बचने की अनुमति देता है।


ObjectTreeNode इंटरफ़ेस की जाँच करें, और आप पाएंगे कि children एक array नहीं बल्कि एक विधि है, इसलिए इस तरह से उन्हें मांग पर उत्पन्न किया जाता है। FileFUSETreeNode और DirectoryFUSETreeNode अमूर्त वर्ग हैं जहाँ कुछ विधियाँ NotSupported त्रुटि फेंकती हैं (स्पष्ट रूप से, FileFUSETreeNode कभी भी readdir लागू नहीं करना चाहिए)।

FUSEफ़ेकेड

UML वर्ग आरेख FUSE सिस्टम के लिए इंटरफेस और उनके संबंधों को दर्शाता है। आरेख में IFUSEHandler, IFUSETreeNode, IFileDescriptorStorage इंटरफेस और FUSEFacade वर्ग शामिल हैं। IFUSEHandler में विशेषता नाम और विधियाँ checkAvailability, create, getattr, readAll, remove और writeAll हैं। IFileDescriptorStorage में get, openRO, openWO और release विधियाँ हैं। IFUSETreeNode IFUSEHandler का विस्तार करता है। FUSEFacade में कन्स्ट्रक्टर, क्रिएट, getattr, open, read, readdir, release, rmdir, safeGetNode, unlink और write विधियाँ शामिल हैं, और यह IFUSETreeNode और IFileDescriptorStorage दोनों के साथ इंटरैक्ट करता है।


FUSEFacade सबसे महत्वपूर्ण क्लास है जो प्रोग्राम के मुख्य तर्क को लागू करता है और विभिन्न भागों को एक साथ बांधता है। node-fuse-bindings में कॉलबैक-आधारित API है, लेकिन FUSEFacade विधियाँ Promise-आधारित API के साथ बनाई गई हैं। इस असुविधा को दूर करने के लिए, मैंने इस तरह का कोड इस्तेमाल किया:

 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 विधियाँ handleResultWrapper में लिपटी हुई हैं। FUSEFacade की प्रत्येक विधि जो पथ का उपयोग करती है, बस पथ को पार्स करती है, पेड़ में एक नोड ढूंढती है, और अनुरोधित विधि को कॉल करती है।


FUSEFacade वर्ग से कुछ विधियों पर विचार करें।

 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; }

एक फ़ाइल डिस्क्रिप्टर

अगला कदम उठाने से पहले, आइए इस बात पर करीब से नज़र डालें कि हमारे प्रोग्राम के संदर्भ में फ़ाइल डिस्क्रिप्टर क्या है।

UML वर्ग आरेख FUSE सिस्टम में फ़ाइल डिस्क्रिप्टर के लिए इंटरफ़ेस और उनके संबंधों को दर्शाता है। आरेख में IFileDescriptor, IFileDescriptorStorage इंटरफ़ेस और ReadWriteFileDescriptor, ReadFileDescriptor और WriteFileDescriptor वर्ग शामिल हैं। IFileDescriptor में बाइनरी, fd, size और readToBuffer, writeToBuffer विधियाँ हैं। IFileDescriptorStorage में get, openRO, openWO और release विधियाँ हैं। ReadWriteFileDescriptor अतिरिक्त कंस्ट्रक्टर, readToBuffer और writeToBuffer विधियों के साथ IFileDescriptor को लागू करता है। ReadFileDescriptor और WriteFileDescriptor ReadWriteFileDescriptor का विस्तार करते हैं, जिसमें ReadFileDescriptor में writeToBuffer विधि होती है और WriteFileDescriptor में readToBuffer विधि होती है

ReadWriteFileDescriptor एक ऐसा वर्ग है जो फ़ाइल डिस्क्रिप्टर को संख्या के रूप में और बाइनरी डेटा को बफर के रूप में संग्रहीत करता है। इस वर्ग में readToBuffer और writeToBuffer विधियाँ हैं जो फ़ाइल डिस्क्रिप्टर बफर में डेटा को पढ़ने और लिखने की क्षमता प्रदान करती हैं। ReadFileDescriptor और WriteFileDescriptor केवल पढ़ने और केवल लिखने वाले डिस्क्रिप्टर के कार्यान्वयन हैं।


IFileDescriptorStorage एक इंटरफ़ेस है जो फ़ाइल डिस्क्रिप्टर स्टोरेज का वर्णन करता है। इस इंटरफ़ेस के लिए प्रोग्राम में केवल एक कार्यान्वयन है: InMemoryFileDescriptorStorage . जैसा कि आप नाम से बता सकते हैं, यह फ़ाइल डिस्क्रिप्टर को मेमोरी में संग्रहीत करता है क्योंकि हमें डिस्क्रिप्टर के लिए दृढ़ता की आवश्यकता नहीं होती है।


आइए देखें कि FUSEFacade फ़ाइल डिस्क्रिप्टर और स्टोरेज का उपयोग कैसे करता है:

 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; }


ऊपर दिया गया कोड सीधा-सादा है। यह फ़ाइल डिस्क्रिप्टर को पढ़ने, लिखने और रिलीज़ करने के तरीकों को परिभाषित करता है, यह सुनिश्चित करता है कि ऑपरेशन करने से पहले फ़ाइल डिस्क्रिप्टर वैध है। रिलीज़ विधि फ़ाइल डिस्क्रिप्टर ऑब्जेक्ट से फ़ाइल सिस्टम नोड में डेटा भी लिखती है और फ़ाइल डिस्क्रिप्टर को मुक्त करती है।


हमने libfuse और ट्री के बारे में कोड लिखना समाप्त कर लिया है। अब समय है इमेज से संबंधित कोड पर आगे बढ़ने का।

छवियाँ: "डेटा ट्रांसफर ऑब्जेक्ट" भाग
UML वर्ग आरेख छवि प्रबंधन के लिए इंटरफेस और उनके संबंधों को दर्शाता है। आरेख में ImageBinary, ImageMeta, Image और IImageMetaStorage इंटरफेस शामिल हैं। ImageBinary में बफर और आकार विशेषताएँ हैं। ImageMeta में id, नाम, originalFileName और originalFileType विशेषताएँ हैं। Image में binary और meta विशेषताएँ हैं, जहाँ binary ImageBinary प्रकार की है और meta ImageMeta प्रकार की है। IImageMetaStorage में create, get, list और remove विधियाँ हैं


ImageMeta एक ऑब्जेक्ट है जो किसी इमेज के बारे में मेटा जानकारी संग्रहीत करता है। IImageMetaStorage एक इंटरफ़ेस है जो मेटा के लिए स्टोरेज का वर्णन करता है। प्रोग्राम में इंटरफ़ेस के लिए केवल एक कार्यान्वयन है: FSImageMetaStorage क्लास एक JSON फ़ाइल में संग्रहीत इमेज मेटाडेटा को प्रबंधित करने के लिए IImageMetaStorage इंटरफ़ेस को लागू करता है।


यह मेमोरी में मेटाडेटा को स्टोर करने के लिए कैश का उपयोग करता है और यह सुनिश्चित करता है कि जब ज़रूरत हो तो JSON फ़ाइल से पढ़कर कैश हाइड्रेटेड हो। यह क्लास इमेज मेटाडेटा बनाने, पुनर्प्राप्त करने, सूचीबद्ध करने और हटाने के लिए विधियाँ प्रदान करता है, और यह अपडेट को बनाए रखने के लिए JSON फ़ाइल में परिवर्तन वापस लिखता है। कैश IO ऑपरेशन की संख्या को कम करके प्रदर्शन में सुधार करता है।


ImageBinary , जाहिर है, एक ऑब्जेक्ट है जिसमें बाइनरी इमेज डेटा है। Image इंटरफ़ेस ImageMeta और ImageBinary का संयोजन है।

छवियाँ: बाइनरी स्टोरेज और जेनरेटर

UML वर्ग आरेख छवि निर्माण और बाइनरी स्टोरेज के लिए इंटरफेस और उनके संबंधों को दर्शाता है। आरेख में IBinaryStorage, IImageGenerator इंटरफेस और FSBinaryStorage, ImageGeneratorComposite, PassThroughImageGenerator, TextImageGenerator और ImageLoaderFacade क्लास शामिल हैं। IBinaryStorage में लोड, रिमूव और राइट विधियाँ हैं। FSBinaryStorage IBinaryStorage को लागू करता है और इसमें एक अतिरिक्त कंस्ट्रक्टर है। IImageGenerator में एक विधि generate है। PassThroughImageGenerator और TextImageGenerator IImageGenerator को लागू करते हैं। ImageGeneratorComposite में addGenerator और generate विधियाँ हैं। ImageLoaderFacade में एक कंस्ट्रक्टर और एक लोड विधि है, और यह IBinaryStorage और IImageGenerator के साथ इंटरैक्ट करता है


IBinaryStorage बाइनरी डेटा स्टोरेज के लिए एक इंटरफ़ेस है। बाइनरी स्टोरेज को इमेज से अनलिंक किया जाना चाहिए और इसमें कोई भी डेटा स्टोर किया जा सकता है: इमेज, वीडियो, JSON या टेक्स्ट। यह तथ्य हमारे लिए महत्वपूर्ण है, और आप देखेंगे कि क्यों।


IImageGenerator एक इंटरफ़ेस है जो जनरेटर का वर्णन करता है। जनरेटर प्रोग्राम का एक महत्वपूर्ण हिस्सा है। यह कच्चा बाइनरी डेटा और मेटा लेता है और उसके आधार पर एक छवि बनाता है। प्रोग्राम को जनरेटर की आवश्यकता क्यों है? क्या प्रोग्राम उनके बिना काम कर सकता है?


यह संभव है, लेकिन जनरेटर कार्यान्वयन में लचीलापन जोड़ देंगे। जनरेटर उपयोगकर्ताओं को चित्र, पाठ डेटा और मोटे तौर पर कहें तो कोई भी डेटा अपलोड करने की अनुमति देता है जिसके लिए आप जनरेटर लिखते हैं।


IImageGenerator इंटरफ़ेस का उपयोग करके टेक्स्ट फ़ाइल को इमेज में बदलने की प्रक्रिया को दर्शाने वाला आरेख। बाईं ओर, 'myfile.txt' लेबल वाली टेक्स्ट फ़ाइल के लिए एक आइकन है, जिसमें 'Hello, world!' लिखा है। 'IImageGenerator' लेबल वाला एक तीर दाईं ओर इंगित करता है, जहाँ 'myfile.png' लेबल वाली इमेज फ़ाइल के लिए एक आइकन है, जिसमें इमेज में 'Hello, world!' लिखा है।


प्रवाह इस प्रकार है: बाइनरी डेटा को स्टोरेज से लोड किया जाता है (ऊपर चित्र में myfile.txt ), और फिर बाइनरी एक जनरेटर के पास जाता है। यह "ऑन द फ्लाई" एक छवि उत्पन्न करता है। आप इसे एक प्रारूप से दूसरे प्रारूप में कनवर्टर के रूप में देख सकते हैं जो हमारे लिए अधिक सुविधाजनक है।


आइये जनरेटर का एक उदाहरण देखें:

 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 क्लास एक ऐसा मुखौटा है जो तार्किक रूप से भंडारण और जनरेटर को जोड़ता है - दूसरे शब्दों में, यह उस प्रवाह को लागू करता है जिसे आपने ऊपर पढ़ा है।

छवियाँ: वेरिएंट

UML वर्ग आरेख छवि निर्माण और बाइनरी स्टोरेज के लिए इंटरफेस और उनके संबंधों को दर्शाता है। आरेख में IBinaryStorage, IImageGenerator इंटरफेस और FSBinaryStorage, ImageGeneratorComposite, PassThroughImageGenerator, TextImageGenerator और ImageLoaderFacade क्लास शामिल हैं। IBinaryStorage में लोड, रिमूव और राइट विधियाँ हैं। FSBinaryStorage IBinaryStorage को लागू करता है और इसमें एक अतिरिक्त कंस्ट्रक्टर है। IImageGenerator में एक विधि generate है। PassThroughImageGenerator और TextImageGenerator IImageGenerator को लागू करते हैं। ImageGeneratorComposite में addGenerator और generate विधियाँ हैं। ImageLoaderFacade में एक कंस्ट्रक्टर और एक लोड विधि है, और यह IBinaryStorage और IImageGenerator के साथ इंटरैक्ट करता है


IImageVariant विभिन्न छवि वेरिएंट बनाने के लिए एक इंटरफ़ेस है। इस संदर्भ में, एक वेरिएंट एक छवि है जो "ऑन द फ्लाई" उत्पन्न होती है जो हमारे फाइल सिस्टम में फ़ाइलों को देखते समय उपयोगकर्ता को दिखाई जाएगी। जनरेटर से मुख्य अंतर यह है कि यह कच्चे डेटा के बजाय एक छवि को इनपुट के रूप में लेता है।


इस प्रोग्राम के तीन वैरिएंट हैं: ImageAlwaysRandom , ImageOriginalVariant , और ImageWithTextImageAlwaysRandom एक यादृच्छिक RGB शोर वर्ग के साथ मूल छवि लौटाता है।


 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 }; } }


मैं NodeJS में छवियों पर काम करने के लिए सबसे सुविधाजनक तरीके के रूप में sharp लाइब्रेरी का उपयोग करता हूं: https://github.com/lovell/sharp


ImageOriginalVariant बिना किसी बदलाव के एक छवि लौटाता है (लेकिन यह एक अलग संपीड़न प्रारूप में एक छवि लौटा सकता है)। ImageWithText एक छवि लौटाता है जिसके ऊपर लिखित पाठ होता है। यह तब मददगार होगा जब हम एक ही छवि के पूर्वनिर्धारित वेरिएंट बनाते हैं। उदाहरण के लिए, अगर हमें एक छवि के 10 यादृच्छिक रूपांतरों की आवश्यकता है, तो हमें इन भिन्नताओं को एक दूसरे से अलग करना होगा।


इसका समाधान यह है कि मूल चित्र के आधार पर 10 चित्र बनाएं, जहां हम प्रत्येक चित्र के ऊपरी बाएं कोने में 0 से 9 तक की क्रमिक संख्या प्रस्तुत करते हैं।

छवियों का एक क्रम जिसमें चौड़ी आँखों वाली एक सफ़ेद और काली बिल्ली दिखाई गई है। छवियों को बाईं ओर 0 से शुरू करके, 1 से बढ़ते हुए, और दाईं ओर 9 तक दीर्घवृत्त के साथ जारी रखते हुए संख्याओं के साथ लेबल किया गया है। प्रत्येक छवि में बिल्ली की अभिव्यक्ति समान रहती है


ImageCacheWrapper का उद्देश्य वेरिएंट से अलग है और यह विशेष IImageVariant क्लास के परिणामों को कैश करके रैपर के रूप में कार्य करता है। इसका उपयोग उन इकाइयों को लपेटने के लिए किया जाएगा जो बदलती नहीं हैं, जैसे कि इमेज कनवर्टर, टेक्स्ट-टू-इमेज जनरेटर, इत्यादि। यह कैशिंग तंत्र तेजी से डेटा पुनर्प्राप्ति को सक्षम बनाता है, मुख्य रूप से तब जब एक ही छवि को कई बार पढ़ा जाता है।


खैर, हमने कार्यक्रम के सभी प्राथमिक भागों को कवर कर लिया है। अब सब कुछ एक साथ जोड़ने का समय है।

वृक्ष की संरचना

छवि प्रबंधन से संबंधित विभिन्न FUSE ट्री नोड्स के बीच पदानुक्रम और संबंधों को दर्शाने वाला UML वर्ग आरेख। कक्षाओं में ImageVariantFileFUSETreeNode, ImageCacheWrapper, ImageItemAlwaysRandomDirFUSETreeNode, ImageItemOriginalDirFUSETreeNode, ImageItemCounterDirFUSETreeNode, ImageManagerItemFileFUSETreeNode, ImageItemDirFUSETreeNode, ImageManagerDirFUSETreeNode, ImagesDirFUSETreeNode और RootDirFUSETreeNode शामिल हैं। प्रत्येक वर्ग में छवि मेटाडेटा, बाइनरी डेटा और फ़ाइल संचालन जैसे create, readAll, writeAll, remove, और getattr से संबंधित विशेषताएँ और विधियाँ होती हैं।


नीचे दिया गया क्लास डायग्राम दर्शाता है कि ट्री क्लास को उनके इमेज समकक्षों के साथ कैसे जोड़ा जाता है। डायग्राम को नीचे से ऊपर की ओर पढ़ा जाना चाहिए। RootDir (मुझे नामों में FUSETreeNode पोस्टफ़िक्स से बचने दें) उस फ़ाइल सिस्टम के लिए रूट डायर है जिसे प्रोग्राम लागू कर रहा है। ऊपरी पंक्ति में जाने पर, दो डायर देखें: ImagesDir और ImagesManagerDirImagesManagerDir में उपयोगकर्ता छवियों की सूची होती है और उन्हें नियंत्रित करने की अनुमति देता है। फिर, ImagesManagerItemFile एक विशेष फ़ाइल के लिए एक नोड है। यह क्लास CRUD ऑपरेशन को लागू करता है।


ImagesManagerDir को नोड के सामान्य कार्यान्वयन के रूप में लें:

 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(); } }


आगे बढ़ते हुए, ImagesDir में उपयोगकर्ता की छवियों के नाम पर उपनिर्देशिकाएँ शामिल हैं। ImagesItemDir प्रत्येक निर्देशिका के लिए ज़िम्मेदार है। इसमें सभी उपलब्ध वेरिएंट शामिल हैं; जैसा कि आपको याद होगा, वेरिएंट की संख्या तीन है। प्रत्येक वेरिएंट एक निर्देशिका है जिसमें विभिन्न प्रारूपों (वर्तमान में: jpeg, png, और webm) में अंतिम छवि फ़ाइलें शामिल हैं। ImagesItemOriginalDir और ImagesItemCounterDir सभी उत्पन्न ImageVariantFile इंस्टेंस को कैश में लपेटते हैं।


यह मूल छवियों के निरंतर पुनः-एन्कोडिंग से बचने के लिए आवश्यक है क्योंकि एन्कोडिंग CPU-उपभोग करती है। आरेख के शीर्ष पर ImageVariantFile है। यह कार्यान्वयन का मुकुट रत्न है और पहले वर्णित IFUSEHandler और IImageVariant की संरचना है। यह वह फ़ाइल है जिसके लिए हमारे सभी प्रयास किए जा रहे हैं।

परिक्षण

आइए परीक्षण करें कि अंतिम फ़ाइल सिस्टम एक ही फ़ाइल के लिए समानांतर अनुरोधों को कैसे संभालता है। ऐसा करने के लिए, हम md5sum उपयोगिता को कई थ्रेड में चलाएँगे, जो फ़ाइल सिस्टम से फ़ाइलें पढ़ेंगे और उनके हैश की गणना करेंगे। फिर, हम इन हैश की तुलना करेंगे। यदि सब कुछ सही ढंग से काम कर रहा है, तो हैश अलग-अलग होने चाहिए।

 #!/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


मैंने स्क्रिप्ट चलाई और निम्नलिखित आउटपुट की जांच की (स्पष्टता के लिए थोड़ा सा साफ किया):

 Run 1... Run 2... Run 3... Run 4... Run 5... wait... bcdda97c480db74e14b8779a4e5c9d64 0954d3b204c849ab553f1f5106d576aa 564eeadfd8d0b3e204f018c6716c36e9 73a92c5ef27992498ee038b1f4cfb05e 77db129e37fdd51ef68d93416fec4f65


बहुत बढ़िया! सभी हैश अलग-अलग हैं, जिसका अर्थ है कि फाइल सिस्टम हर बार एक अद्वितीय छवि लौटाता है!

निष्कर्ष

मुझे उम्मीद है कि इस लेख ने आपको अपना खुद का FUSE कार्यान्वयन लिखने के लिए प्रेरित किया है। याद रखें, इस प्रोजेक्ट का स्रोत कोड यहाँ उपलब्ध है: https://github.com/pinkiesky/node-fuse-images


हमने जो फाइल सिस्टम बनाया है, उसे FUSE और Node.js के साथ काम करने के मूल सिद्धांतों को प्रदर्शित करने के लिए सरल बनाया गया है। उदाहरण के लिए, यह सही तिथियों को ध्यान में नहीं रखता है। इसमें सुधार की बहुत गुंजाइश है। उपयोगकर्ता GIF फ़ाइलों से फ़्रेम निष्कर्षण, वीडियो ट्रांसकोडिंग, या यहां तक कि वर्कर्स के माध्यम से कार्यों को समानांतर करने जैसी कार्यक्षमताओं को जोड़ने की कल्पना करें।


हालाँकि, पूर्णता अच्छे की दुश्मन है। आपके पास जो है, उसी से शुरुआत करें, उसे काम में लाएँ और फिर दोहराएँ। हैप्पी कोडिंग!