सुरुवातीचा वेब प्लेन टेक्स्टवर (plain text) चालत असे. तुम्ही फॉर्ममध्ये युजरनेम किंवा कमेंट टाईप करायचा, सबमिट बटण दाबायचा आणि एक छोटी स्ट्रिंग HTTP द्वारे सर्व्हरकडे पाठवली जायची. त्या साध्या रिक्वेस्ट-रिस्पॉन्स सायकलने (request-response cycle) इन्फ्रास्ट्रक्चरची व्याख्या केली होती. जेव्हा लोकांना फोटो, डॉक्युमेंट्स आणि व्हिडिओ शेअर करायचे लागले, तेव्हा इंजिनिअर्सना संपूर्णपणे वाचण्यायोग्य टेक्स्टसाठी बनवलेल्या सिस्टममधून रॉ बायनरी डेटा (raw binary data) कसा हलवायचा, हे शोधून काढावे लागले.
जर तुम्ही एखाद्या JSON ऑब्जेक्टमध्ये JPEG टाकण्याचा प्रयत्न केला, तर तुम्हाला एका मूलभूत अडथळ्याचा सामना करावा लागेल. JSON हा एक टेक्स्ट प्रोटोकॉल आहे. तो वैध Unicode कॅरेक्टर्स, कोट्स (quotes), ब्रॅसेस (braces) आणि योग्यरित्या एस्केप केलेल्या स्ट्रिंग्सची (escaped strings) अपेक्षा करतो. बायनरी फाईल म्हणजे फक्त बाइट्सचा (bytes) एक लांब क्रम असतो, ज्यापैकी अनेकचे कोणतेही प्रिंट करण्यायोग्य स्वरूप (printable representation) नसते. जर तुम्ही हे बाइट्स JSON स्ट्रिंगमध्ये कोंबले, तर पार्सर (parser) बिघडतो, एस्केप सीक्वेन्स (escape sequences) पेलोडला (payload) खराब करतात आणि दुसरीकडे संपूर्ण मेसेज वाचता येत नाही.
Base64 एन्कोडिंग हा एक स्पष्ट उपाय म्हणून समोर आला. ते बायनरी डेटाला साठ (sixty-four) प्रिंट करण्यायोग्य ASCII कॅरेक्टर्सच्या मर्यादित संचात पुन्हा मॅप करते. बायनरीमधील प्रत्येक तीन बाइट्सचे चार टेक्स्ट कॅरेक्टर्समध्ये रूपांतर होते. आता पेलोड हा कायदेशीर JSON बनतो, ज्याचा अर्थ असा की तो कोणत्याही स्टँडर्ड API मधून सुरक्षितपणे प्रवास करू शकतो. पण याची किंमत लगेच चुकवावी लागते. त्या री-एन्कोडिंगमुळे फाईलचा आकार साधारणतः ३३ टक्क्यांनी वाढतो. तीन मेगाबाइटची इमेज वायरवर चार मेगाबाइटची होते. डेटा पुन्हा पुन्हा ट्रान्सलेट करण्यासाठी क्लायंट आणि सर्व्हर दोघांनाही अतिरिक्त CPU सायकल खर्च कराव्या लागतात. अधिक महत्त्वाचे म्हणजे, अनेक JSON सर्व्हर फ्रेमवर्क्स पार्स करण्यापूर्वी संपूर्ण बॉडी मेमरीमध्ये वाचतात. काही मोठ्या फाईल्स एकाच वेळी अपलोड केल्या तर एखादा मध्यम स्वरूपाचा सर्व्हर कोलमडून जाऊ शकतो, कारण प्रत्येक फाईल डिस्कवर सेव्ह होण्यापूर्वी मेमरीमध्ये (RAM) एक जड टेक्स्ट स्ट्रिंग म्हणून धरून ठेवली जाते. Base64 आपत्कालीन परिस्थितीत काम करते, पण ते मोठ्या प्रमाणावर (production scale) जड फाईल्स वाहून नेण्यासाठी कधीच डिझाइन केलेले नव्हते.
अधिक चांगला पर्याय म्हणजे multipart/form-data. हे फॉरमॅट एका सिंगल HTTP रिक्वेस्टला वेगवेगळ्या भागांच्या संग्रहाप्रमाणे मानतो, ज्यातील प्रत्येक भाग एका युनिक बाउंड्री स्ट्रिंगने (boundary string) विभागलेला असतो. एक भाग प्लेन टेक्स्ट फील्ड असू शकतो. पुढचा भाग स्वतःच्या Content-Type आणि Content-Disposition हेडर्ससह रॉ बायनरी इमेज असू शकतो. सर्व्हर येणारा स्ट्रीम (stream) क्रमाने वाचतो, बाउंड्री मार्कर्स शोधतो आणि संपूर्ण पेलोडला टेक्स्टचा एक मोठा ब्लॉक म्हणून न हाताळता प्रत्येक विभाग योग्य हँडलरकडे (handler) सोपवतो.
Node.js मध्ये, हा फरक विशेषतः महत्त्वाचा आहे. express.json() मिडलवेअरला JSON बॉडीज पार्स कसे करायचे हे माहित आहे, परंतु ते फाईल स्ट्रीम्स हाताळत नाही. multipart अपलोड्स प्रोसेस करण्यासाठी, तुम्हाला Multer किंवा Busboy सारख्या स्ट्रीमिंग पार्सरची (streaming parser) गरज असते. ही टूल्स रॉ रिक्वेस्ट स्ट्रीमला जोडली जातात आणि ती तुकड्या तुकड्याने (chunk by chunk) वाचतात. उदाहरणार्थ, Multer तुम्हाला येणाऱ्या फाईल्स डिस्कवरील तात्पुरत्या फोल्डरमध्ये लिहायच्या की लहान फाईल्स मेमरीमध्ये ठेवायच्या, हे निवडू देते. हा कॉन्फिगरेशन निर्णय महत्त्वाचा आहे. जर तुम्ही सर्व काही मेमरीमध्ये ठेवले आणि तुमच्या ॲपला अचानक एकाच वेळी अनेक मोठ्या फाईल्स मिळाल्या, तर तुमचा प्रोसेस 'हीप स्पेस' (heap space) संपून क्रॅश होऊ शकतो. डिस्कवर लिहिणे हे स्थिरतेसाठी I/O चा वापर करते, परंतु यामुळे क्लीनअप आणि पाथ सिक्युरिटी (path security) यांसारखे प्रश्न निर्माण होतात.
लहान ॲप्लिकेशनसाठी, फाईल्स ./uploads सारख्या लोकल फोल्डरमध्ये सेव्ह करणे नैसर्गिक आणि जलद वाटते. फाईल तुमच्या कोडच्या रन होत असलेल्या त्याच मशीनवर येते आणि ती सर्व्ह करणे म्हणजे फक्त योग्य पाथकडे (path) निर्देश करणे असते. जोपर्यंत सर्व काही व्यवस्थित चालते, तोपर्यंत हे काम करते.
ज्या क्षणी तुम्ही दुसऱ्या ॲप्लिकेशन सर्व्हरच्या समोर लोड बॅलन्सर (load balancer) लावता, त्या क्षणी लोकल स्टोरेज ही एक त्रुटी (bug) बनते. एखादा युजर प्रोफाइल पिक्चर अपलोड करतो. लोड बॅलन्सर ती रिक्वेस्ट सर्व्हर A कडे वळवतो आणि फाईल सर्व्हर A च्या डिस्कवर लिहिली जाते. नंतर, तो युजर इमेज पाहण्यासाठी विचारतो, पण लोड बॅलन्सर ती रिक्वेस्ट सर्व्हर B कडे पाठवतो. सर्व्हर B स्वतःचे फाईलसिस्टम तपासतो आणि त्याला काहीही सापडत नाही. फाईल प्रभावीपणे गायब झालेली असते. युजरला एकाच मशीनवर स्थिर ठेवण्यासाठी तुम्ही 'स्टिकी सेशन्स' (sticky sessions) लागू करू शकता, पण तो एक नाजूक उपाय आहे. जर सर्व्हर A रीस्टार्ट झाला, पुन्हा डिप्लॉय झाला किंवा ऑटोस्केलिंग इन्स्टन्सद्वारे बदलला गेला, तर डेटा नाहीसा होतो. कंटेनरायझ्ड (containerized) वातावरणात, लोकल डिस्क अधिक क्षणभंगुर (ephemeral) असतात. डॉकर कंटेनरचे (Docker container) फाईलसिस्टम हे विल्हेवाट लावण्यासाठी (disposable) बनवलेले असते. त्याला कायमस्वरूपी स्टोरेज म्हणून वापरणे हा युजरचा डेटा गमावण्याचा एक खात्रीशीर मार्ग आहे.
याचे प्रमाणित समाधान म्हणजे कॉम्प्युट (compute) आणि स्टोरेज (storage) वेगळे करणे. तुम्ही तुमचे ॲप्लिकेशन सर्व्हर्स स्टेटलेस (stateless) ठेवता आणि अपलोड केलेल्या फाईल्स AWS S3 किंवा Google Cloud Storage सारख्या समर्पित ऑब्जेक्ट स्टोरेजकडे पाठवता. या सेवा टिकाऊपणा (durability), भौगोलिक वितरण (geographic distribution) आणि प्रचंड कॉनकरन्सीसाठी (concurrency) बनवल्या गेल्या आहेत. ॲप्लिकेशन सर्व्हर रिक्वेस्ट हाताळतो, मेटाडेटा (metadata) व्हॅलिडेट करतो आणि त्यानंतर बाइट्स अशा इन्फ्रास्ट्रक्चरकडे सोपवतो जे विशेषतः त्यांना साठवण्यासाठी डिझाइन केलेले आहे.
तरीही, जर तुम्ही हे स्वरूप निष्काळजीपणे लागू केले, तर ते अडथळा (bottleneck) निर्माण करू शकते. अनेक टीम्स ब्राउझरद्वारे फाईल backend ला अपलोड करून आणि त्यानंतर backend द्वारे प्रत्येक बाईट object storage कडे फॉरवर्ड करून सुरुवात करतात. जर एखाद्या वापरकर्त्याने पाचशे मेगाबाइटचा व्हिडिओ अपलोड केला, तर तुमचा सर्व्हर मध्यस्थ (middleman) बनतो. फाईल आत खेचण्यासाठी तो बँडविड्थ वापरतो आणि नंतर ती S3 कडे पाठवण्यासाठी अधिक बँडविड्थ वापरतो. ट्रान्सफरच्या संपूर्ण कालावधीसाठी कनेक्शन उघडे राहते. खराब नेटवर्क असलेल्या वापरकर्त्यांकडून होणारे संथ अपलोड सर्व्हर कनेक्शन्सना मिनिटांनु मिनिटे अडकवून ठेवू शकतात. जर सर्व्हरने स्ट्रीम बफर केली, तर मेमरीचा वापर वाढलेला राहतो आणि जर तुम्ही metered hosting वापरत असाल, तर तुम्हाला एकाच डेटा ट्रान्सफरसाठी दोनदा पैसे मोजावे लागतात. Horizontal scaling मुळे हे सुटत नाही, कारण तुम्ही जोडलेला प्रत्येक अतिरिक्त सर्व्हर अजूनही अशा बाईट्सची ने-आण करण्यात अडकलेला असतो ज्यांची त्याला गरज नसते.
आधुनिक प्रणाली डेटा पाथमधून backend ला पूर्णपणे काढून टाकून ही समस्या सोडवतात. फाईल स्वीकारण्याऐवजी, backend फक्त अपलोड करण्यासाठी परवानगीची विनंती स्वीकारते. ही प्रक्रिया अशी दिसते:
- ब्राउझर backend ला अपलोड सुरू करण्यास सांगतो, सहसा फक्त फाईलचे नाव, फाईलचा प्रकार आणि अपेक्षित उद्देश पाठवतो.
- backend वापरकर्त्याची ओळख पटवते (authenticates), बिझनेस नियमांनुसार विनंतीची पडताळणी करते आणि object storage प्रदात्याकडून तात्पुरते presigned URL तयार करण्यासाठी SDK वापरते.
- backend ती URL ब्राउझरला परत पाठवते. ही URL एका विशिष्ट bucket आणि key साठी मर्यादित असते, पाच किंवा पंधरा मिनिटांसारख्या अल्प कालावधीसाठी वैध असते आणि केवळ आवश्यक असलेल्या नेमक्या परवानग्या देणाऱ्या टोकनने स्वाक्षरी केलेली (signed) असते.
- ब्राउझर मानक PUT किंवा POST वापरून थेट S3 किंवा GCS वर फाईल अपलोड करतो. बाईट्स वापरकर्त्याच्या डिव्हाइसवरून थेट...
