first commit
This commit is contained in:
9
.env
Normal file
9
.env
Normal 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
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
exports/*
|
||||||
|
logs/*
|
||||||
|
node_modules
|
||||||
|
uploads/*
|
||||||
23
config/init.js
Normal file
23
config/init.js
Normal 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
17
config/logger.js
Normal 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
2
generate-token.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
const crypto = require('crypto')
|
||||||
|
console.log(crypto.randomBytes(48).toString("hex"))
|
||||||
1914
package-lock.json
generated
Normal file
1914
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal 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
195
server.js
Normal 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"));
|
||||||
Reference in New Issue
Block a user