first commit

This commit is contained in:
2026-03-26 08:17:49 +03:30
commit 0fb6597e55
8 changed files with 2191 additions and 0 deletions

9
.env Normal file
View File

@@ -0,0 +1,9 @@
CDN_UPLOAD_URL=http://localhost:4000/upload
CDN_SERVICE_TOKEN=5075761248974997bf50fdf2a136e2d27320c15358d59ab09e37438b3ee7f08e1d15770a114a8774bf90e14189560832
CDN_PUBLIC_BASE=http://localhost:4000
CDN_WEBHOOK_SECRET=webhook-secret
MAX_PROFILE_SIZE=2097152 # 2MB
MAX_DOCUMENT_SIZE=10485760 # 10MB
ALLOWED_PROFILE_TYPES=["image/jpeg","image/png","image/webp"]
ALLOWED_DOCUMENT_TYPES=["application/pdf","application/zip","application/dicom","application/octet-stream"]
PORT=4000

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
exports/*
logs/*
node_modules
uploads/*

23
config/init.js Normal file
View File

@@ -0,0 +1,23 @@
const {default: rateLimit} = require("express-rate-limit");
const path = require("path");
require("dotenv").config();
module.exports = {
PORT: process.env.PORT || 4000,
JWT_SECRET:
process.env.JWT_SECRET || "dasdG23qewqe1234441fFGfdhdghnnbCCZXQSDQWEweqwe",
STORAGE_PATH: path.resolve(__dirname, "../uploads"),
MAX_FILE_SIZE: 5 * 1024 * 1024, // 5MB
ALLOWED_FILE_TYPES: ["image/jpeg", "image/png", "image/webp"], // نوع‌های مجاز
limiter: rateLimit({
windowMs: 5 * 60 * 1000, // 5 دقیقه
max: 30,
standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
handler: (req, res) => {
res.status(500).json({
status: 500,
message: "تعداد درخواست ها بیش تر از حد مجاز، در فرصتی دیگر تلاش کنید",
});
},
}),
};

17
config/logger.js Normal file
View File

@@ -0,0 +1,17 @@
const path = require("path");
const fs = require("fs");
const winston = require("winston");
const logDirectory = path.join(__dirname, "..", "logs");
if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDirectory);
}
const logger = winston.createLogger({
level: "info",
format: winston.format?.json(),
transports: [
new winston.transports.File({filename: path.join(logDirectory, "cdn.log")}),
],
});
module.exports = logger;

2
generate-token.js Normal file
View File

@@ -0,0 +1,2 @@
const crypto = require('crypto')
console.log(crypto.randomBytes(48).toString("hex"))

1914
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "cdn-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"busboy": "^1.6.0",
"compression": "^1.8.1",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"express-rate-limit": "^8.2.1",
"file-type": "^21.1.1",
"multer": "^2.0.2",
"winston": "^3.19.0"
},
"devDependencies": {
"nodemon": "^3.1.11"
}
}

195
server.js Normal file
View File

@@ -0,0 +1,195 @@
const express = require("express");
const fs = require("fs");
const os = require("os");
const path = require("path");
const process = require("process");
const logger = require("./config/logger");
const compression = require("compression");
const multer = require("multer");
const dotenv = require("dotenv");
const { limiter } = require("./config/init");
const { performance } = require("perf_hooks");
performance.eventLoopUtilization();
dotenv.config();
const app = express();
const UPLOAD_DIR = path.join(__dirname, "uploads");
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
const EXPORTS_DIR = path.join(__dirname, "exports");
if (!fs.existsSync(EXPORTS_DIR)) fs.mkdirSync(EXPORTS_DIR, { recursive: true });
const upload_files = multer({
storage: multer.diskStorage({
destination: (_, __, cb) => cb(null, UPLOAD_DIR),
filename: (_, file, cb) => cb(null, `${Date.now()}-${file.originalname}`),
}),
});
const upload_exports = multer({
storage: multer.diskStorage({
destination: (_, __, cb) => cb(null, EXPORTS_DIR),
filename: (_, file, cb) => cb(null, `${Date.now()}-${file.originalname}`),
}),
});
const serviceStartTime = Date.now();
app.use(compression());
// app.use((req, res, next) => {
// logger.info({
// method: req.method,
// url: req.url,
// timestamp: new Date().toISOString(),
// });
// next();
// });
app.post("/upload", upload_files.single("file"), (req, res) => {
// Authorization
logger.info({
method: req.method,
url: req.url,
timestamp: new Date().toISOString(),
});
const auth = req.headers["authorization"]?.split(" ")[1];
if (auth !== process.env.CDN_SERVICE_TOKEN) {
return res.status(401).json({ error: "Unauthorized" });
}
if (!req.file) return res.status(400).json({ error: "No file uploaded" });
const publicUrl = `${process.env.CDN_PUBLIC_BASE}/uploads/${req.file.filename}`;
return res.json({
url: publicUrl,
size: req.file.size,
mime: req.file.mimetype,
});
});
app.post("/upload-exports", upload_exports.single("exports"), (req, res) => {
// Authorization
// console.log(req);
logger.info({
method: req.method,
url: req.url,
timestamp: new Date().toISOString(),
});
const auth = req.headers["authorization"]?.split(" ")[1];
if (auth !== process.env.CDN_SERVICE_TOKEN) {
return res.status(401).json({ error: "Unauthorized" });
}
if (!req.file) return res.status(400).json({ error: "No file uploaded" });
const publicUrl = `${process.env.CDN_PUBLIC_BASE}/exports/${req.file.filename}`;
return res.json({
url: publicUrl,
size: req.file.size,
mime: req.file.mimetype,
});
});
app.post("/upload/delete", (req, res) => {
logger.info({
method: req.method,
url: req.url,
timestamp: new Date().toISOString(),
});
const auth = req.headers["authorization"];
const bearer = auth?.split(" ")[1];
if (bearer !== process.env.CDN_SERVICE_TOKEN) {
return res.status(401).json({ error: "Unauthorized" });
}
console.log(req);
const { fileKey, fileUrl } = req.body;
const fileName = fileKey ?? fileUrl?.split("/").pop();
if (!fileName) return res.status(400).json({ error: "file key/url missing" });
const savePath = path.join(UPLOAD_DIR, fileName);
if (!fs.existsSync(savePath))
return res.status(404).json({ error: "Not found" });
fs.unlinkSync(savePath);
return res.json({ success: true, file: fileName });
});
app.use("/uploads", express.static(UPLOAD_DIR, { maxAge: 3600 }));
app.use("/exports", express.static(EXPORTS_DIR, { maxAge: 3600 }));
app.get("/status", (req, res) => {
try {
const mem = process.memoryUsage();
const cpu = process.cpuUsage();
const elu = performance.eventLoopUtilization();
res.status(200).json({
status: "OK",
system: {
service: {
uptime: Math.floor(process.uptime()),
pid: process.pid,
nodeVersion: process.version,
},
memory: {
rss: mem.rss,
heapUsed: mem.heapUsed,
heapTotal: mem.heapTotal,
external: mem.external,
},
cpu: {
user: cpu.user,
system: cpu.system,
},
eventLoop: {
utilization: +elu.utilization.toFixed(4),
active: elu.active,
idle: elu.idle,
},
system: {
freeMemory: os.freemem(),
totalMemory: os.totalmem(),
loadAvg: os.loadavg(),
cpuCores: os.cpus().length,
},
timestamp: new Date().toISOString(),
},
});
} catch (error) {
res.status(500).json({
status: "ERROR",
message: "خطای داخلی سرویس",
error: error.message,
});
}
});
app.get("/logs", (req, res) => {
const logFilePath = path.join(__dirname, "logs", "cdn.log");
fs.readFile(logFilePath, "utf8", (err, data) => {
if (err) {
return res.status(500).json({
status: "ERROR",
message: "لاگ خوانده نشد",
error: err.message,
});
}
return res.status(200).json({
status: "OK",
logs: data.split("\n").filter((line) => line),
});
});
});
app.delete("/logs/clear", (req, res) => {
const logFilePath = path.join(__dirname, "logs", "cdn.log");
fs.truncate(logFilePath, 0, (err) => {
if (err) {
return res
.status(500)
.json({ status: "ERROR", message: "خطا در پاکسازی لاگ ها" });
}
return res
.status(200)
.json({ status: "OK", message: "لاگ ها با موفقیت پاک شدند" });
});
});
app.listen(4000, () => console.log("CDN server listening on 4000"));