Firebase এর হাতেখড়ি পর্ব ৯-এ Firebase Storage দিয়ে ফাইল আপলোড শিখেছিলাম। এই পর্বে দেখব Cloudflare-এর নিজস্ব স্টোরেজ সার্ভিস — R2 — যা প্রায় একই কাজ করে, কিন্তু একটা বড় পার্থক্য নিয়ে আসে: egress ফি নেই, অর্থাৎ ফাইল যতবারই ডাউনলোড হোক না কেন, তার জন্য আলাদা কোনো বিল আসে না।
R2 কী এবং egress ফি সমস্যাটা কী
বেশিরভাগ ক্লাউড স্টোরেজ (Amazon S3, এমনকি Firebase Storage-এর ভিত্তিও Google Cloud Storage) দুই ধরনের খরচ নেয় — ফাইল রাখার জন্য (storage) আর ফাইল বের করে নেওয়ার জন্য (egress/bandwidth)। একটা ওয়েবসাইটে ছবি অনেকবার দেখা হলে egress খরচ দ্রুত বেড়ে যেতে পারে। R2 এই egress ফি সম্পূর্ণ বাদ দিয়েছে — শুধু storage-এর জন্য খরচ, ডাউনলোডের জন্য না।
R2 vs Firebase Storage — সংক্ষিপ্ত তুলনা
| বিষয় | Firebase Storage | Cloudflare R2 |
|---|---|---|
| Egress ফি | আছে (কোটা পার হলে) | নেই |
| ফ্রন্টএন্ড SDK | সরাসরি ব্রাউজার থেকে আপলোড সহজ | সাধারণত Worker-এর মাধ্যমে |
| Auth ইন্টিগ্রেশন | Firebase Auth-এর সাথে সরাসরি যুক্ত | নিজে বসাতে হয় (Worker-এ) |
| সেরা ব্যবহার | Auth-যুক্ত ছোট-মাঝারি প্রজেক্ট | বড় ফাইল, বেশি ডাউনলোড হওয়া কনটেন্ট (বই, ভিডিও) |
Pathok Ghar-এর মতো একটা প্রজেক্টে, যেখানে বইয়ের PDF/EPUB বারবার ডাউনলোড হয়, সেখানে R2-এর "egress ফি নেই" সুবিধাটা সরাসরি কাজে লাগে।
একটা Bucket তৈরি করা
R2-তে ফাইল রাখার জায়গাকে বলে Bucket — Firebase Storage-এর রুট ফোল্ডারের মতোই একটা ধারণা।
- Cloudflare Dashboard-এ R2-এ যান
- Create bucket চাপুন
- একটা নাম দিন (যেমন
book-covers) - Location — Automatic রাখাই ভালো, Cloudflare নিজেই সবচেয়ে কাছের জায়গা বেছে নেবে
Worker-এর সাথে Bucket যুক্ত করা
R2 ব্যবহার করতে হলে Worker-এর wrangler.toml ফাইলে একটা binding যোগ করতে হয় — এটাই Worker-কে বলে দেয় কোন bucket-এ অ্যাক্সেস আছে।
# wrangler.toml name = "my-worker" main = "src/index.js" [[r2_buckets]] binding = "BOOK_COVERS" # কোডে env.BOOK_COVERS দিয়ে অ্যাক্সেস হবে bucket_name = "book-covers"
ফাইল আপলোড করা (PUT)
Worker-এর মধ্যে env.BOOK_COVERS একটা অবজেক্ট, যার নিজস্ব মেথড আছে — put(), get(), delete() — অনেকটা Firebase এর হাতেখড়ি পর্ব ৬-এর ref()-এর মতোই কনসেপ্ট, শুধু ফাইলের জন্য।
export default {
async fetch(request, env, ctx) {
if (request.method === "PUT") {
const url = new URL(request.url);
const key = url.pathname.slice(1); // যেমন "book1.jpg"
await env.BOOK_COVERS.put(key, request.body);
return new Response(`${key} আপলোড হয়েছে`);
}
return new Response("শুধু PUT সাপোর্টেড", { status: 405 });
},
};
ফাইল পড়া (GET)
export default {
async fetch(request, env, ctx) {
if (request.method === "GET") {
const url = new URL(request.url);
const key = url.pathname.slice(1);
const object = await env.BOOK_COVERS.get(key);
if (!object) {
return new Response("ফাইল পাওয়া যায়নি", { status: 404 });
}
return new Response(object.body, {
headers: { "content-type": object.httpMetadata.contentType || "application/octet-stream" },
});
}
return new Response("শুধু GET সাপোর্টেড", { status: 405 });
},
};
ফাইল ডিলিট করা
if (request.method === "DELETE") {
const url = new URL(request.url);
const key = url.pathname.slice(1);
await env.BOOK_COVERS.delete(key);
return new Response(`${key} মুছে ফেলা হয়েছে`);
}
সবকিছু একসাথে — একটা ছোট ফাইল সার্ভার
পর্ব ৫-এর রুটিং প্যাটার্ন দিয়ে GET/PUT/DELETE একসাথে সাজানো —
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
if (request.method === "PUT") {
await env.BOOK_COVERS.put(key, request.body);
return new Response(`${key} আপলোড হয়েছে`, { status: 201 });
}
if (request.method === "GET") {
const object = await env.BOOK_COVERS.get(key);
if (!object) return new Response("পাওয়া যায়নি", { status: 404 });
return new Response(object.body, {
headers: { "content-type": object.httpMetadata?.contentType || "application/octet-stream" },
});
}
if (request.method === "DELETE") {
await env.BOOK_COVERS.delete(key);
return new Response(`${key} মুছে ফেলা হয়েছে`);
}
return new Response("সাপোর্টেড না", { status: 405 });
},
};
Public Access — সরাসরি URL দিয়ে ফাইল দেখানো
প্রতিবার Worker দিয়ে ঘুরিয়ে না এনে, Bucket-কে সরাসরি পাবলিক করে একটা .r2.dev URL বা কাস্টম ডোমেইন দিয়েও অ্যাক্সেস দেওয়া যায় —Dashboard-এ Bucket Settings → Public access → Allow Access চালু করলেই।
সতর্কতা: Public access চালু করলে bucket-এর ভেতরের যেকোনো ফাইল URL অনুমান করে যে কেউ দেখতে পারবে (যদি না ফাইলের নাম ইউনিক/র্যান্ডম রাখা হয়) — স্পর্শকাতর ফাইলে এটা এড়িয়ে চলা ভালো, বরং Worker দিয়ে অ্যাক্সেস নিয়ন্ত্রণ (যেমন Auth চেক করে) রাখা নিরাপদ।
ইন্টারেক্টিভ — R2 বনাম Firebase Storage খরচ তুলনা
একটা বইয়ের কভার মাসে কতবার দেখা হচ্ছে তার ওপর নির্ভর করে egress খরচ কীভাবে বদলায় দেখো (সরলীকৃত উদাহরণ)।
ভালো অভ্যাস
- Public access সাবধানে চালু করুন — স্পর্শকাতর ফাইলে Worker দিয়ে অ্যাক্সেস নিয়ন্ত্রণ রাখুন
- ফাইলের key/নাম ইউনিক রাখুন — Firebase এর হাতেখড়ি পর্ব ৯-এর মতোই, একই নামের ফাইল ওভাররাইট হয়ে যায়
- বড়, বেশি-ডাউনলোড-হওয়া কনটেন্টে R2 প্রাধান্য দিন — ছোট প্রজেক্টে Firebase Storage-ও ঠিকঠাক চলে, egress ফ্রি কোটার মধ্যে থাকলে পার্থক্য বোঝা যায় না
নিজে চেষ্টা করো
Exercise:
- Cloudflare Dashboard-এ একটা R2 bucket তৈরি করো, নিজের Worker প্রজেক্টে
wrangler.toml-এ binding যোগ করো। - উপরের PUT/GET/DELETE কোড দিয়ে একটা ছবি আপলোড করো, তারপর সেই URL দিয়ে ফিরে পেয়ে দেখো।
- Bucket-এ Public access চালু করে
.r2.devURL দিয়ে সরাসরি ফাইল খুলে দেখো। - নিজের কোনো প্রজেক্টের জন্য চিন্তা করো — কোন ফাইল R2-তে (বেশি ডাউনলোড, বড় সাইজ) আর কোনগুলো Firebase Storage-এই (Auth-যুক্ত, ছোট) রাখা উচিত।
পরের পর্বে যা থাকছে
পরের পর্বে আমরা শিখব KV পরিচিতি — একটা দ্রুত কী-ভ্যালু স্টোরেজ, যা ক্যাশিং, সেটিংস, বা ফ্ল্যাগ রাখার জন্য RTDB বা R2-এর চেয়ে অনেক বেশি উপযোগী।