Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const redisClient = require("./redis");
const authenticateToken = require("./middleware/authenticateToken");
const cors = require("@koa/cors");
require("./models");
require("./schedules/fileRecover");
require("dotenv").config({ path: ".env.local" });

const app = new Koa();
Expand Down Expand Up @@ -59,5 +60,4 @@ app.listen(process.env.SERVER_PORT, async () => {
await redisClient.connect();
await sequelize.sync();
console.log(`Server is running on ${process.env.INTERNAL_NETWORK_DOMAIN}`);
console.log(`Server is running on ${process.env.PUBLIC_NETWORK_DOMAIN}`);
});
12 changes: 11 additions & 1 deletion models/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,20 @@ const Files = sequelize.define(
allowNull: true,
defaultValue: null,
},
is_delete: {
is_deleted: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
deleted_at: {
type: DataTypes.DATE,
allowNull: true,
},
deleted_by: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null,
},
real_file_location: {
type: DataTypes.STRING(255),
allowNull: true,
Expand All @@ -96,6 +105,7 @@ const Files = sequelize.define(
tableName: "files",
timestamps: false,
underscored: true,
paranoid: true,
charset: "utf8mb4",
collate: "utf8mb4_general_ci",
}
Expand Down
7 changes: 6 additions & 1 deletion models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,12 @@ const Users = sequelize.define(
disk_size: {
type: DataTypes.DOUBLE,
allowNull: true,
defaultValue: 0,
defaultValue: 100000000,
},
used_capacity: {
type: DataTypes.DOUBLE,
allowNull: true,
defaultValue: 100000000,
},
is_admin: {
type: DataTypes.BOOLEAN,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"koa-static": "^5.0.0",
"koa2-cors": "^2.0.6",
"mysql2": "^3.10.1",
"node-cron": "^3.0.3",
"nodemon": "^3.1.4",
"path-to-regexp": "^7.0.0",
"pm2": "^5.4.0",
Expand Down
44 changes: 25 additions & 19 deletions routers/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ router.post(
is_public: isFilePublic,
thumb_location: thumbUrl,
is_thumb: shouldGenerateThumb,
is_delete: false,
is_deleted: false,
real_file_thumb_location: realThumbPath,
mime,
ext: fileExt,
Expand Down Expand Up @@ -200,7 +200,7 @@ router.get("/files", validateQuery(FILES_LIST_GET_QUERY), async (ctx) => {

const { rows, count } = await Files.findAndCountAll({
where: {
is_delete: false,
is_deleted: false,
[Op.or]: [
{ public_expiration: null, is_public: true },
{ public_expiration: { [Op.gt]: new Date() }, is_public: true },
Expand Down Expand Up @@ -250,7 +250,7 @@ router.get("/files/:id", validateParams(FILES_REST_ID), async (ctx) => {
const file = await Files.findOne({
where: {
id,
is_delete: false,
is_deleted: false,
[Op.or]: [
{ public_expiration: null, is_public: true },
{ public_expiration: { [Op.gt]: new Date() }, is_public: true },
Expand Down Expand Up @@ -301,7 +301,7 @@ router.put("/files/:id", validateParams(FILES_REST_ID), async (ctx) => {
const file = await Files.findOne({
where: {
id,
is_delete: false,
is_deleted: false,
created_by: ctx.state.user.id,
},
});
Expand Down Expand Up @@ -338,12 +338,14 @@ router.delete("/files/:id", validateParams(FILES_REST_ID), async (ctx) => {
const { id } = ctx.params;

try {
const deleted_by = ctx.state.user.id;
// 查找文件
const file = await Files.findOne({
where: {
id,
is_delete: false,
created_by: ctx.state.user.id,
is_deleted: false,
deleted_at: new Date(),
deleted_by,
},
});

Expand All @@ -353,11 +355,11 @@ router.delete("/files/:id", validateParams(FILES_REST_ID), async (ctx) => {
return;
}

// 执行软删除,将 is_delete 字段设置为 true
// 执行软删除,将 is_deleted 字段设置为 true
await file.update({
is_delete: true,
updated_at: new Date(), // 更新更新时间
updated_by: ctx.query.updated_by || "anonymous", // 可以通过查询参数传递更新者
is_deleted: true,
deleted_at: new Date(), // 更新更新时间
deleted_by, // 可以通过查询参数传递更新者
});

// 返回删除成功的信息
Expand All @@ -372,7 +374,7 @@ router.delete("/files/:id", validateParams(FILES_REST_ID), async (ctx) => {
// 文件批量删除接口
router.delete("/files", validateBody(FILES_BODY_BATCH_IDS), async (ctx) => {
const { ids } = ctx.request.body;
const updated_by = ctx.state.user.id;
const deleted_by = ctx.state.user.id;

if (!ids || !Array.isArray(ids) || ids.length === 0) {
ctx.status = 400;
Expand All @@ -384,17 +386,21 @@ router.delete("/files", validateBody(FILES_BODY_BATCH_IDS), async (ctx) => {
// 查找并更新指定的文件
const [numberOfAffectedRows] = await Files.update(
{
is_delete: true,
updated_by: updated_by,
updated_at: new Date(),
is_deleted: true,
deleted_by,
deleted_at: new Date(),
},
{
where: {
id: {
[Op.in]: ids,
},
created_by: ctx.state.user.id,
is_delete: false,
is_deleted: false,
[Op.or]: [
{
created_by: deleted_by,
},
],
},
}
);
Expand Down Expand Up @@ -422,7 +428,7 @@ router.get("/files/:id/preview", validateParams(FILES_REST_ID), async (ctx) => {
const file = await Files.findOne({
where: {
id,
is_delete: false,
is_deleted: false,
[Op.or]: [
{ public_expiration: null, is_public: true },
{ public_expiration: { [Op.gt]: new Date() }, is_public: true },
Expand Down Expand Up @@ -487,7 +493,7 @@ router.get(
const file = await Files.findOne({
where: {
id: id,
is_delete: false,
is_deleted: false,
[Op.or]: [
{ public_expiration: null, is_public: true },
{ public_expiration: { [Op.gt]: new Date() }, is_public: true },
Expand Down Expand Up @@ -550,7 +556,7 @@ router.post(
const files = await Files.findAll({
where: {
id: { [Op.in]: ids },
is_delete: false,
is_deleted: false,
[Op.or]: [
{ public_expiration: null, is_public: true },
{ public_expiration: { [Op.gt]: new Date() }, is_public: true },
Expand Down
25 changes: 21 additions & 4 deletions routers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ const Router = require("koa-router");
const redisClient = require("../redis");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const { filesize } = require("filesize");
const checkAdminAuth = require("../middleware/checkAdminAuth");
require("dotenv").config({ path: ".env.local" });
const Users = require("../models/users");
const { USERS_LOGIN_POST, USER_REST_ID } = require("../types/schema/users");
const {
USERS_LOGIN_POST,
USER_REST_PARAMS_PATCH,
} = require("../types/schema/users");

const { validateBody, validateParams } = require("../types");
const { USER_STATUS, USER_ACTION_TYPES } = require("../constants/users");

Expand Down Expand Up @@ -77,9 +82,18 @@ router.post("/users", validateBody(USERS_LOGIN_POST), async (ctx) => {
const { id, disk_size, status, created_at, login_at } = await Users.create({
username,
password: hashedPassword,
created_by: ctx.state?.user?.id ?? null,
});

ctx.status = 201;
ctx.body = { id, disk_size, status, created_at, username, login_at };
ctx.body = {
id,
disk_size: filesize(disk_size),
status,
created_at,
username,
login_at,
};
} catch (error) {
console.error(error);
ctx.status = 500;
Expand All @@ -94,7 +108,10 @@ router.get("/users/info", async (ctx) => {
});
if (user) {
ctx.status = 200; // 确保状态码为 200
ctx.body = user.dataValues;
ctx.body = {
...user.dataValues,
disk_size: filesize(user.dataValues?.disk_size),
};
return;
}
if (!user) {
Expand Down Expand Up @@ -139,7 +156,7 @@ router.delete("/sessions", async (ctx) => {
// 禁用用户
router.patch(
"/users/:id/:action",
validateParams(USER_REST_ID),
validateParams(USER_REST_PARAMS_PATCH),
checkAdminAuth,
async (ctx) => {
const { id, action } = ctx.params;
Expand Down
92 changes: 92 additions & 0 deletions schedules/fileRecover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const cron = require("node-cron");
const { Op } = require("sequelize");
const fs = require("fs").promises;
const Files = require("../models/files");
const Users = require("../models/users");

// 定时任务每天22:52分运行一次
cron.schedule("52 23 * * *", async () => {
const sevenDaysAgo = new Date(new Date() - 7 * 24 * 60 * 60 * 1000);

try {
// 查找需要删除的记录
const records = await Files.findAll({
where: {
is_deleted: true,
deleted_at: {
[Op.lt]: sevenDaysAgo, // 查找 deletedAt 字段小于七天前的记录
},
},
});

// 收集所有文件删除和用户容量更新的Promise
const fileDeletePromises = [];
const userUpdates = {};

for (const record of records) {
const {
real_file_location,
real_file_thumb_location,
created_by,
file_size,
} = record;

if (real_file_location) {
fileDeletePromises.push(
fs.unlink(real_file_location).catch((err) => {
console.error(`Failed to delete file: ${real_file_location}`, err);
})
);
}

if (real_file_thumb_location) {
fileDeletePromises.push(
fs.unlink(real_file_thumb_location).catch((err) => {
console.error(
`Failed to delete thumbnail: ${real_file_thumb_location}`,
err
);
})
);
}

if (userUpdates[created_by]) {
userUpdates[created_by] -= file_size;
} else {
userUpdates[created_by] =
(
await Users.findOne({
where: { id: created_by },
})
).used_capacity - file_size;
}
}

// 批量删除文件
await Promise.all(fileDeletePromises);

// 批量更新用户容量
const userUpdatePromises = [];
for (const [userId, newCapacity] of Object.entries(userUpdates)) {
userUpdatePromises.push(
Users.update({ used_capacity: newCapacity }, { where: { id: userId } })
);
}
await Promise.all(userUpdatePromises);

// 真实删除数据库记录
await Files.destroy({
where: {
is_deleted: true,
deleted_at: {
[Op.lt]: sevenDaysAgo,
},
},
force: true, // 真实删除
});

console.log("Old records and associated files deleted successfully.");
} catch (error) {
console.error("Error deleting old records and associated files:", error);
}
});
9 changes: 7 additions & 2 deletions types/schema/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ const USERS_LOGIN_POST = Joi.object({
password: Joi.string().required(),
});

const USER_REST_ID = Joi.object({
const USER_REST_PARAMS_PATCH = Joi.object({
id: Joi.string().required(),
action: Joi.string()
.valid(...Object.keys(USER_ACTION_TYPES))
.required(),
});

const USER_REST_ID = Joi.object({
id: Joi.string().required(),
});

module.exports = {
USER_REST_ID,
USER_REST_PARAMS_PATCH,
USERS_LOGIN_POST,
USER_REST_ID,
};
Loading