چگونه یک فایل در پایگاه داده ذخیره می‌شود

خلاصه
1405/03/14

## 📂 چگونه می‌توان یک فایل را در پایگاه‌داده (Database) ذخیره کرد؟ در بیشتر برنامه‌های تحت وب یا دسکتاپ، دو روش اصلی برای نگهداری اطلاعات مربوط به یک فایل وجود دارد: | روش |

چگونه یک فایل در پایگاه داده ذخیره می‌شود

## 📂 چگونه می‌توان یک فایل را در پایگاه‌داده (Database) ذخیره کرد؟

در بیشتر برنامه‌های تحت وب یا دسکتاپ، دو روش اصلی برای نگهداری اطلاعات مربوط به یک فایل وجود دارد:

| روش | توضیح | مزایا | معایب |
|-----|--------|-------|------|
| **۱. ذخیره‌سازی باینری (BLOB) داخل DB** | محتویات فایل به صورت یک مقدار باینری (`BLOB`, `BYTEA`, `VARBINARY` و …) در یک سطر جدول ذخیره می‌شود. | * تراکنش‌پذیری کامل (commit/rollback)
* پشتیبان‌گیری/بازیابی یکپارچه (backup)
* امنیت ساده (دسترسی به داده فقط از طریق DB) | * حجم بزرگ جدول → سرعت بکاپ/ریستور کاهش می‌یابد
* مصرف RAM/IO سرور DB بیشتر
* بعضی DB‌ها محدودیت حجم BLOB دارند |
| **۲. ذخیره‌سازی فیزیکی در سیستم فایل + ذخیره مسیر در DB** | فایل در یک پوشه (مثلاً `/uploads/2024/06/`) ذخیره می‌شود؛ در دیتابیس فقط مسیر یا نام فایل ذخیره می‌شود. | * کارایی به‌خصوص برای فایل‌های بزرگ (سرور فایل سیستمی سریع‌تر است)
* مقیاس‌پذیری آسان (می‌توانید از CDN یا سرویس‌های ابری مثل S3 استفاده کنید)
* حجم DB ثابت می‌ماند | * نیاز به حفظ هماهنگی بین فایل‑سیستم و DB (transactional consistency سخت‌تر)
* پیکربندی دسترسی‌های امنیتی دوگانه (DB + FS)
* بکاپ جداگانه برای دو بخش |

در ادامه جزئیات هر روش، نکات عملی، و مثال‌های کدنویسی برای برخی DB‌های رایج (MySQL, PostgreSQL, SQL Server, SQLite) آورده شده است.

---

## 🔎 ۱. ذخیره‌سازی باینری (BLOB) داخل دیتابیس

### ساختار جدول نمونه

```sql
-- MySQL / MariaDB
CREATE TABLE documents (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
mime_type VARCHAR(100) NOT NULL,
data LONGBLOB NOT NULL, -- حداکثر 4 GB
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

```sql
-- PostgreSQL
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
mime_type TEXT NOT NULL,
data BYTEA NOT NULL, -- می‌تواند تا 1 GB+ باشد
uploaded_at TIMESTAMPTZ DEFAULT now()
);
```

```sql
-- SQL Server
CREATE TABLE Documents (
Id BIGINT IDENTITY PRIMARY KEY,
Name NVARCHAR(255) NOT NULL,
MimeType NVARCHAR(100) NOT NULL,
Data VARBINARY(MAX) NOT NULL,
UploadedAt DATETIME2 DEFAULT SYSDATETIME()
);
```

### عملیات اصلی

| عملیات | توضیح | نمونه کد (Python + SQLAlchemy) |
|--------|-------|--------------------------------|
| **Insert** (بارگذاری) | فایل را به باینری می‌خوانید و در فیلد `data` می‌گذارید. | ```python
with open('pdf.pdf','rb') as f:
data = f.read()
new_doc = Document(name='pdf.pdf', mime_type='application/pdf', data=data)
session.add(new_doc)
session.commit()
``` |
| **Select** (دریافت) | داده باینری برگردانده می‌شود؛ می‌توانید مستقیماً به مرورگر بفرستید یا روی دیسک بنویسید. | ```python
doc = session.query(Document).filter_by(id=5).first()
with open('out.pdf','wb') as f:
f.write(doc.data)
``` |
| **Update** | می‌توانید فقط `data` یا متادیتای دیگر را بروز کنید. | (مانند Insert فقط با `session.merge` یا `update`.) |
| **Delete** | حذف سطر باعث حذف باینری می‌شود. | ```python
session.query(Document).filter_by(id=5).delete()
session.commit()
``` |

### نکات مهم برای BLOB

| نکته | توضیح |
|------|-------|
| **اندازه‌گذاری** | در MySQL از `LONGBLOB` (تا 4 GB) استفاده کنید؛ در PostgreSQL `BYTEA` محدود به 1 GB+ (بسته به `max_allowed_packet` یا تنظیمات `work_mem`). |
| **Chunking** | برای فایل‌های خیلی بزرگ (مثلاً > 100 MB) می‌توانید به‌جای یک BLOB بزرگ، آن را به قطعات کوچکتر (مثلاً 5 MB) تقسیم کنید و در جدول جداگانه (`document_chunks`) ذخیره کنید. این کار باعث بهبود کارایی خواندن/نوشتن می‌شود. |
| **کاهش حجم** | اگر فشرده‌سازی مناسب است (مثلاً متنی یا JSON)، می‌توانید قبل از ذخیره‌سازی `gzip` کنید؛ سپس مقدار `Content-Encoding` را ذخیره کنید. |
| **Stream‑API** | برخی DB‌ها (مانند PostgreSQL با `psycopg2`/`asyncpg`) امکان streaming BLOB را می‌دهند تا تمام بایت‌ها یک‌باره به حافظه نیاید. |
| **پشتیبان‌گیری** | حتماً از ابزارهای مخصوص DB (mysqldump, pg_dump) استفاده کنید؛ فایلی که در BLOB است، با بقیه داده‌ها همان‌طور پشتیبان می‌شود. |

---

## 🔎 ۲. ذخیره‌سازی در سیستم فایل + مسیر در DB

### معماری پیشنهادی

1. **بارگذاری** → سرور فایل را در یک مسیر امن ذخیره می‌کند (معمولاً یک پوشه‌ی خارج از `wwwroot` برای جلوگیری از دسترسی مستقیم).
2. **نام‌گذاری امن** → برای جلوگیری از تداخل نام، از یک شناسه یکتا (UUID یا hash) استفاده کنید.
3. **ذخیره مسیر** → مسیر نسبی یا مطلق (مثلاً `uploads/2024/06/550e8400-e29b-41d4-a716-446655440000.pdf`) را در یک جدول متادیتا ذخیره می‌کنید.
4. **دسترسی** → هنگام درخواست دانلود، برنامه مسیر را می‌خواند و فایل را با هدرهای مناسب (`Content-Type`, `Content-Disposition`) می‌فرستد یا از CDN سرو می‌کند.

### جدول متادیتا نمونه

```sql
CREATE TABLE file_meta (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) NOT NULL UNIQUE, -- یا BINARY(16) در MySQL
original_name VARCHAR(255) NOT NULL,
mime_type VARCHAR(100) NOT NULL,
size_bytes BIGINT NOT NULL,
path VARCHAR(500) NOT NULL, -- مسیر نسبی داخل storage_root
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

### مثال عملی (Node.js + Express + Multer)

```javascript
const express = require('express');
const multer = require('multer');
const crypto = require('crypto');
const path = require('path');
const {Pool} = require('pg'); // PostgreSQL

const app = express();
const pool = new Pool({connectionString: process.env.DATABASE_URL});

// تنظیمات Multer: ذخیره‌سازی به‌صورت سفارشی
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = path.join(__dirname, 'uploads', new Date().toISOString().slice(0,7));
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uuid = crypto.randomUUID(); // e.g. '550e8400-e29b-41d4-a716-446655440000'
const ext = path.extname(file.originalname);
cb(null, `${uuid}${ext}`);
}
});
const upload = multer({ storage });

app.post('/upload', upload.single('file'), async (req, res) => {
const file = req.file;
const uuid = path.parse(file.filename).name; // استخراج uuid از نام فایل

// ذخیره‌سازی اطلاعات متادیتا در DB
await pool.query(
`INSERT INTO file_meta (uuid, original_name, mime_type, size_bytes, path)
VALUES ($1,$2,$3,$4,$5)`,
[uuid, file.originalname, file.mimetype, file.size,
path.relative(__dirname, file.path)]
);

res.json({ id: uuid, message: 'فایل با موفقیت ذخیره شد' });
});

app.get('/download/:uuid', async (req, res) => {
const {uuid} = req.params;
const {rows} = await pool.query(
`SELECT original_name, mime_type, path FROM file_meta WHERE uuid=$1`,
[uuid]
);
if (!rows.length) return res.status(404).send('فایل یافت نشد');

const {original_name, mime_type, path: relPath} = rows[0];
const absolutePath = path.join(__dirname, relPath);
res.download(absolutePath, original_name, {headers: {'Content-Type': mime_type}});
});

app.listen(3000,()=>console.log('Listening on :3000'));
```

### مزایای استفاده از این روش

| مزیت | توضیح |
|------|-------|
| **سرعت** | سیستم‌عامل برای خواندن/نوشتن فایل‌های بزرگ بهینه‌تر از DB است. |
| **مقیاس‌پذیری** | می‌توانید مسیر ذخیره را به سرویس‌های ابری (Amazon S3, Azure Blob, Google Cloud Storage) یا CDN‌ها تغییر دهید؛ فقط متادیتا در DB می‌ماند. |
| **قابلیت کش** | می‌توانید از کش‌های لایه‌ای (Redis, Varnish) برای سرو کردن فایل‌ها استفاده کنید. |
| **پشتیبان‌گیری مستقل** | می‌توانید فایل‌ها را با ابزارهای بکاپ فایل (rsync, borg) جداگانه از DB بکاپ بگیرید. |

### چالش‌ها و راه‌حل‌ها

| چالش | راه‌حل |
|------|--------|
| **هماهنگی DB ↔️ FS** | همه عملیات (INSERT + ذخیره‌سازی فایل) را داخل یک تراکنش برنامه‌ای (به‌عنوان مثال با `transaction` در ORM) انجام دهید؛ اگر ذخیره‌سازی فایل به هر دلیلی شکست، تراکنش DB را rollback کنید. |
| **حذف فایل‌های معلق** | اسکریپت دوره‌ای (Cron) اجرا کنید که ردیف‌های DB را با فایل‌های موجود مقایسه کند و فایل‌های بدون ردیف را حذف کند (یا بالعکس). |
| **دسترسی غیرمجاز** | مسیر ذخیره‌سازی را خارج از ریشه‌ی سرویس وب (مثلاً خارج از `public/`) قرار دهید؛ فقط از طریق کد برنامه اجازه بارگیری بدهید. |
| **نام‌گذاری امن** | از UUID یا هش SHA‑256 محتوا استفاده کنید؛ از ترکیب تاریخ هم برای توزیع بهتر در ساب‌دایرکتوری‌ها. |
| **پشتیبان‌گیری همزمان** | در اسکریپت بکاپ، ابتدا DB را dump کنید، سپس پوشه‌ی `uploads/` را با همان snapshot (مثلاً `rsync --link-dest`) کپی کنید تا همدست بمانند. |

---

## 📚 کد نمونه برای ذخیره BLOB در چند پایگاه داده معروف

### 1️⃣ MySQL (PHP PDO)

```php
$pdo = new PDO('mysql:host=localhost;dbname=mydb;charset=utf8mb4', 'user', 'pwd');

// Insert
$stmt = $pdo->prepare("INSERT INTO documents (name,mime_type,data) VALUES (:n,:t,:d)");
$stmt->bindParam(':n', $filename);
$stmt->bindParam(':t', $mime);
$stmt->bindParam(':d', $blob, PDO::PARAM_LOB);
$blob = file_get_contents('/path/to/file.pdf');
$stmt->execute();

// Retrieve
$stmt = $pdo->prepare("SELECT name,mime_type,data FROM documents WHERE id = ?");
$stmt->execute([$id]);
$stmt->bindColumn('data', $stream, PDO::PARAM_LOB);
$stmt->fetch(PDO::FETCH_BOUND);
header("Content-Type: $mime");
echo $stream;
?>
```

### 2️⃣ PostgreSQL (Node.js `pg`)

```javascript
const {Pool}=require('pg');
const fs=require('fs');
const pool=new Pool({connectionString:process.env.DATABASE_URL});

async function saveFile(id, filePath){
const data = await fs.promises.readFile(filePath);
await pool.query(
`INSERT INTO documents (id, name, mime_type, data) VALUES ($1,$2,$3,$4)`,
[id, path.basename(filePath), 'application/pdf', data]
);
}
```

### 3️⃣ SQL Server (C# ADO.NET)

```csharp
using (SqlConnection cn = new SqlConnection(connStr))
{
cn.Open();
using (SqlCommand cmd = new SqlCommand(
"INSERT INTO Documents (Name, MimeType, Data) VALUES (@n,@t,@d)", cn))
{
cmd.Parameters.Add("@n", SqlDbType.NVarChar).Value = "photo.jpg";
cmd.Parameters.Add("@t", SqlDbType.NVarChar).Value = "image/jpeg";
cmd.Parameters.Add("@d", SqlDbType.VarBinary, -1).Value = File.ReadAllBytes(@"C:\temp\photo.jpg");
cmd.ExecuteNonQuery();
}
}
```

---

## 🛠️ چه زمانی از کدام روش استفاده کنیم؟

| شرایط | توصیه |
|-------|--------|
| **فایل‌های کوچک (< 1 MB) و تعداد کم** | ذخیره در BLOB سرجانه است؛ ساده‌ترین پیاده‌سازی. |
| **فایل‌های بزرگ یا تعداد زیاد** | ذخیره در فایل‑سیستم یا سرویس ابری + مسیر در DB. |
| **نیاز به تراکنش‌پذیری قوی (Insert + Delete باید atomic باشد)** | BLOB یا ترکیب “two‑phase commit” در برنامه (مثلاً ابتدا ذخیره فایل، سپس commit DB؛ در صورت شکست حذف فایل). |
| **استفاده از CDN یا توزیع جغرافیایی** | حتماً مسیر ذخیره در DB و فایل‌ها را در سرویس CDN (S3, CloudFront, Azure Blob) بگذارید. |
| **محدودیت فضای DB (مانند SQLite یا دیتابیس‌های Embedded)** | استفاده از سیستم فایل ترجیحی است؛ SQLite حتی BLOB بزرگ را بسیار کند می‌کند. |

---

## 📌 نکات امنیتی نهایی

1. **تایید نوع فایل** – حتی اگر MIME ارسال شده صحیح باشد، با `file` یا کتابخانه‌های تشخیص محتوای باینری (مانند `magic` در Python) بررسی کنید.
2. **اسکن ویروس** – برای بارگذاری‌های عمومی، از اسکن AV (ClamAV, VirusTotal API) قبل از ذخیره‌سازی استفاده کنید.
3. **محافظت از مسیر** – اگر از فایل‑سیستم استفاده می‌کنید، دسترسی مستقیم به پوشه را از وب سرور مسدود کنید (`chmod 750` + `Options -Indexes`).
4. **کش کردن هدرها** – برای فایل‌های ثابت می‌توانید `ETag`/`Last‑Modified` تنظیم کنید تا مرورگرها از دانلود دوباره جلوگیری کنند.
5. **رمزنگاری در‑حافظه** – اگر داده‌های حساس (مانند اسکن‌های کارت شناسایی) ذخیره می‌کنید، می‌توانید قبل از ذخیره‌سازی BLOB را با AES‑256 رمزگذاری کنید؛ کلید را به‌صورت امن (مثلاً در Vault یا KMS) نگهدارید.

---

### جمع‌بندی

| روش | کی مناسب است؟ |
|-----|----------------|
| **BLOB داخل DB** | فایل‌های کوچک، نیاز به تراکنش‌پذیری کامل، برنامه‌های داخلی (ERP, CRM) که همه چیز در DB نگهداری می‌شود. |
| **فایل‑سیستم + مسیر در DB** | فایل‌های بزرگ، مقیاس‌پذیری، CDN/Cloud Storage، برنامه‌های وب عمومی یا موبایلی. |

با توجه به نیازهای پروژه‌تان (حجم فایل، تعداد درخواست، امنیت، هزینه‌ها) یکی از این دو الگو را انتخاب کنید و نکات پیاده‌سازی و امنیتی بالا را در نظر بگیرید. موفق باشید! 🚀