mirror of
https://github.com/wgh136/nysoure.git
synced 2025-12-16 07:51:14 +00:00
Compare commits
13 Commits
070b9c7656
...
0395bc4686
| Author | SHA1 | Date | |
|---|---|---|---|
| 0395bc4686 | |||
| b1c01431fc | |||
| c55a6612bd | |||
| 9f5f2c6e47 | |||
| c554a05b60 | |||
| 4a60ad6133 | |||
| 67070fee4d | |||
| b794d4cc96 | |||
| e04fd8ceb1 | |||
| c9a5f096bd | |||
| 8b340ab175 | |||
| 327fd72be0 | |||
| 1c23bf1d6e |
@@ -9,14 +9,22 @@ services:
|
||||
- app_data:/var/lib/nysoure
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
environment:
|
||||
- DB_HOST=db
|
||||
- DB_PORT=3306
|
||||
- DB_USER=nysoure
|
||||
- DB_PASSWORD=nysoure_password
|
||||
- DB_NAME=nysoure
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- BANNED_REDIRECT_DOMAINS=example.com,example.org
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
db:
|
||||
image: mariadb:latest
|
||||
@@ -27,9 +35,21 @@ services:
|
||||
- MYSQL_DATABASE=nysoure
|
||||
- MYSQL_USER=nysoure
|
||||
- MYSQL_PASSWORD=nysoure_password
|
||||
ports:
|
||||
- "3306"
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
app_data:
|
||||
|
||||
96
frontend/package-lock.json
generated
96
frontend/package-lock.json
generated
@@ -15,7 +15,6 @@
|
||||
"masonic": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.5.3",
|
||||
@@ -29,7 +28,7 @@
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/spark-md5": "^3.0.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"daisyui": "^5.0.35",
|
||||
"daisyui": "^5.5.5",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
@@ -281,15 +280,6 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
|
||||
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz",
|
||||
@@ -2558,9 +2548,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "5.0.35",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.35.tgz",
|
||||
"integrity": "sha512-AWi11n/x5++mps55jcwrBf0Lmip1euWY0FYcH/05SFGmoqrU7S7/aIUWaiaeqlJ5EcmEZ/7zEY73aOxMv6hcIg==",
|
||||
"version": "5.5.5",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.5.tgz",
|
||||
"integrity": "sha512-ekvI93ZkWIJoCOtDl0D2QMxnWvTejk9V5nWBqRv+7t0xjiBXqAK5U6o6JE2RPvlIC3EqwNyUoIZSdHX9MZK3nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -3618,15 +3608,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-url-attributes": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
||||
@@ -3670,38 +3651,6 @@
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.1.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.1.1.tgz",
|
||||
"integrity": "sha512-FZcp3vk3PXc8onasbsWYahfeDIWX4LkKr4vd01xeXrmqyNXlVNtVecEIw2K1o8z3xYrHMcd1bwYQub+3g7zqCw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -5681,32 +5630,6 @@
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz",
|
||||
"integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||
@@ -6418,7 +6341,7 @@
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -6727,15 +6650,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/spark-md5": "^3.0.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"daisyui": "^5.0.35",
|
||||
"daisyui": "^5.5.5",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
|
||||
@@ -258,6 +258,9 @@ export const i18nData = {
|
||||
"Survival time": "存活时间",
|
||||
"Characters": "角色",
|
||||
"Aliases (one per line)": "别名(每行一个)",
|
||||
"File Size": "文件大小",
|
||||
"Tag": "标签",
|
||||
"Optional": "可选",
|
||||
},
|
||||
},
|
||||
"zh-TW": {
|
||||
@@ -517,6 +520,11 @@ export const i18nData = {
|
||||
"Private": "私有",
|
||||
"View {count} more replies": "查看另外 {count} 條回覆",
|
||||
"Survival time": "存活時間",
|
||||
"Characters": "角色",
|
||||
"Aliases (one per line)": "別名(每行一個)",
|
||||
"File Size": "檔案大小",
|
||||
"Tag": "標籤",
|
||||
"Optional": "可選",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,10 +3,7 @@ import { Response } from "./models.ts";
|
||||
|
||||
const KunApi = {
|
||||
isAvailable(): boolean {
|
||||
return (
|
||||
window.location.hostname === "res.nyne.dev" ||
|
||||
window.location.hostname.startsWith("localhost")
|
||||
);
|
||||
return true;
|
||||
},
|
||||
|
||||
async getPatch(id: string): Promise<Response<KunPatchResponse>> {
|
||||
@@ -16,8 +13,10 @@ const KunApi = {
|
||||
return status === 200 || status === 404; // Accept only 200 and 404 responses
|
||||
},
|
||||
});
|
||||
const uri = `https://www.moyu.moe/api/hikari?vndb_id=${id}`;
|
||||
const uriBase64 = btoa(uri);
|
||||
const res = await client.get(
|
||||
`https://www.moyu.moe/api/hikari?vndb_id=${id}`,
|
||||
`/api/proxy?uri=${uriBase64}`,
|
||||
);
|
||||
if (res.status === 404) {
|
||||
return {
|
||||
|
||||
@@ -123,6 +123,7 @@ export interface RFile {
|
||||
hash?: string;
|
||||
storage_name?: string;
|
||||
created_at: number; // unix timestamp
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
export interface UploadingFile {
|
||||
|
||||
@@ -479,6 +479,7 @@ class Network {
|
||||
fileSize: number,
|
||||
resourceId: number,
|
||||
storageId: number,
|
||||
tag: string,
|
||||
): Promise<Response<UploadingFile>> {
|
||||
return this._callApi(() =>
|
||||
axios.post(`${this.apiBaseUrl}/files/upload/init`, {
|
||||
@@ -487,6 +488,7 @@ class Network {
|
||||
file_size: fileSize,
|
||||
resource_id: resourceId,
|
||||
storage_id: storageId,
|
||||
tag,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -529,6 +531,9 @@ class Network {
|
||||
description: string,
|
||||
resourceId: number,
|
||||
redirectUrl: string,
|
||||
fileSize: number,
|
||||
md5: string,
|
||||
tag: string,
|
||||
): Promise<Response<RFile>> {
|
||||
return this._callApi(() =>
|
||||
axios.post(`${this.apiBaseUrl}/files/redirect`, {
|
||||
@@ -536,6 +541,9 @@ class Network {
|
||||
description,
|
||||
resource_id: resourceId,
|
||||
redirect_url: redirectUrl,
|
||||
file_size: fileSize,
|
||||
md5,
|
||||
tag,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -546,6 +554,7 @@ class Network {
|
||||
description: string,
|
||||
resourceId: number,
|
||||
storageId: number,
|
||||
tag: string,
|
||||
): Promise<Response<RFile>> {
|
||||
return this._callApi(() =>
|
||||
axios.post(`${this.apiBaseUrl}/files/upload/url`, {
|
||||
@@ -554,6 +563,7 @@ class Network {
|
||||
description,
|
||||
resource_id: resourceId,
|
||||
storage_id: storageId,
|
||||
tag,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -566,11 +576,13 @@ class Network {
|
||||
fileId: string,
|
||||
filename: string,
|
||||
description: string,
|
||||
tag: string,
|
||||
): Promise<Response<RFile>> {
|
||||
return this._callApi(() =>
|
||||
axios.put(`${this.apiBaseUrl}/files/${fileId}`, {
|
||||
filename,
|
||||
description,
|
||||
tag,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -201,6 +201,7 @@ class UploadingManager extends Listenable {
|
||||
resourceID: number,
|
||||
storageID: number,
|
||||
description: string,
|
||||
tag: string,
|
||||
onFinished: () => void,
|
||||
): Promise<Response<void>> {
|
||||
const res = await network.initFileUpload(
|
||||
@@ -209,6 +210,7 @@ class UploadingManager extends Listenable {
|
||||
file.size,
|
||||
resourceID,
|
||||
storageID,
|
||||
tag,
|
||||
);
|
||||
if (!res.success) {
|
||||
return {
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function EditResourcePage() {
|
||||
/>
|
||||
<div className={"h-4"}></div>
|
||||
<p className={"my-1"}>{t("Alternative Titles")}</p>
|
||||
{altTitles.map((title, index) => {
|
||||
{altTitles && altTitles.map((title, index) => {
|
||||
return (
|
||||
<div key={index} className={"flex items-center my-2"}>
|
||||
<input
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
@@ -198,7 +199,7 @@ export default function ResourcePage() {
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<h1 className={"text-2xl font-bold px-4 py-2"}>{resource.title}</h1>
|
||||
{resource.alternativeTitles.map((e, i) => {
|
||||
{resource.alternativeTitles && resource.alternativeTitles.map((e, i) => {
|
||||
return (
|
||||
<h2
|
||||
key={i}
|
||||
@@ -801,6 +802,11 @@ function FileTile({ file }: { file: RFile }) {
|
||||
{file.storage_name}
|
||||
</Badge>
|
||||
)}
|
||||
{file.tag && (
|
||||
<Badge className={"badge-soft badge-warning text-xs mr-2"}>
|
||||
{file.tag}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge className={"badge-soft badge-info text-xs mr-2"}>
|
||||
<MdOutlineAccessTime size={16} className={"inline-block"} />
|
||||
{new Date(file.created_at * 1000).toISOString().substring(0, 10)}
|
||||
@@ -919,11 +925,72 @@ function Files({
|
||||
files: RFile[];
|
||||
resource: ResourceDetails;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
|
||||
|
||||
// Extract unique tags from all files
|
||||
const allTags = useMemo(() => {
|
||||
const tags = new Set<string>();
|
||||
files.forEach((file) => {
|
||||
if (file.tag) {
|
||||
tags.add(file.tag);
|
||||
}
|
||||
});
|
||||
return Array.from(tags).sort();
|
||||
}, [files]);
|
||||
|
||||
// Filter files based on selected tags
|
||||
const filteredFiles = useMemo(() => {
|
||||
if (selectedTags.size === 0) {
|
||||
return files;
|
||||
}
|
||||
return files.filter((file) => file.tag && selectedTags.has(file.tag));
|
||||
}, [files, selectedTags]);
|
||||
|
||||
const toggleTag = (tag: string) => {
|
||||
setSelectedTags((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(tag)) {
|
||||
newSet.delete(tag);
|
||||
} else {
|
||||
newSet.add(tag);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={"pt-3"}>
|
||||
{files.map((file) => {
|
||||
{allTags.length > 0 && (
|
||||
<form className="filter mb-4">
|
||||
{allTags.map((tag) => (
|
||||
<input
|
||||
key={tag}
|
||||
className="btn"
|
||||
type="checkbox"
|
||||
aria-label={tag}
|
||||
checked={selectedTags.has(tag)}
|
||||
onChange={() => toggleTag(tag)}
|
||||
/>
|
||||
))}
|
||||
{selectedTags.size > 0 && (
|
||||
<input
|
||||
className="btn btn-square"
|
||||
type="reset"
|
||||
value="×"
|
||||
onClick={() => setSelectedTags(new Set())}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
{filteredFiles.map((file) => {
|
||||
return <FileTile file={file} key={file.id}></FileTile>;
|
||||
})}
|
||||
{filteredFiles.length === 0 && selectedTags.size > 0 && (
|
||||
<div className="text-center text-base-content/60 py-8">
|
||||
{t("No files match the selected tags")}
|
||||
</div>
|
||||
)}
|
||||
<div className={"h-2"}></div>
|
||||
{(app.canUpload() || (app.allowNormalUserUpload && app.isLoggedIn())) && (
|
||||
<div className={"flex flex-row-reverse"}>
|
||||
@@ -954,6 +1021,10 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
const [storage, setStorage] = useState<Storage | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [tag, setTag] = useState<string>("");
|
||||
const [fileSize, setFileSize] = useState<string>("");
|
||||
const [fileSizeUnit, setFileSizeUnit] = useState<string>("MB");
|
||||
const [md5, setMd5] = useState<string>("");
|
||||
|
||||
const [fileUrl, setFileUrl] = useState<string>("");
|
||||
|
||||
@@ -985,11 +1056,38 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
let fileSizeNum = 0;
|
||||
if (fileSize) {
|
||||
const size = parseFloat(fileSize);
|
||||
if (isNaN(size)) {
|
||||
setError(t("File size must be a number"));
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
// Convert to bytes based on unit
|
||||
switch (fileSizeUnit) {
|
||||
case "B":
|
||||
fileSizeNum = size;
|
||||
break;
|
||||
case "KB":
|
||||
fileSizeNum = size * 1024;
|
||||
break;
|
||||
case "MB":
|
||||
fileSizeNum = size * 1024 * 1024;
|
||||
break;
|
||||
case "GB":
|
||||
fileSizeNum = size * 1024 * 1024 * 1024;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const res = await network.createRedirectFile(
|
||||
filename,
|
||||
description,
|
||||
resourceId,
|
||||
redirectUrl,
|
||||
fileSizeNum,
|
||||
md5,
|
||||
tag,
|
||||
);
|
||||
if (res.success) {
|
||||
setSubmitting(false);
|
||||
@@ -1014,6 +1112,7 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
resourceId,
|
||||
storage.id,
|
||||
description,
|
||||
tag,
|
||||
() => {
|
||||
if (mounted.current) {
|
||||
reload();
|
||||
@@ -1046,6 +1145,7 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
description,
|
||||
resourceId,
|
||||
storage.id,
|
||||
tag,
|
||||
);
|
||||
if (res.success) {
|
||||
setSubmitting(false);
|
||||
@@ -1119,15 +1219,7 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
<p className={"text-sm font-bold p-2"}>{t("Type")}</p>
|
||||
<form className="filter mb-2">
|
||||
<input
|
||||
className="btn btn-square"
|
||||
type="reset"
|
||||
value="×"
|
||||
onClick={() => {
|
||||
setFileType(null);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="btn text-sm"
|
||||
className="btn"
|
||||
type="radio"
|
||||
name="type"
|
||||
aria-label={t("Redirect")}
|
||||
@@ -1136,7 +1228,7 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="btn text-sm"
|
||||
className="btn"
|
||||
type="radio"
|
||||
name="type"
|
||||
aria-label={t("Upload")}
|
||||
@@ -1145,7 +1237,7 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="btn text-sm"
|
||||
className="btn"
|
||||
type="radio"
|
||||
name="type"
|
||||
aria-label={t("File Url")}
|
||||
@@ -1153,6 +1245,14 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
setFileType(FileType.serverTask);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="btn btn-square"
|
||||
type="reset"
|
||||
value="×"
|
||||
onClick={() => {
|
||||
setFileType(null);
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
|
||||
{fileType === FileType.redirect && (
|
||||
@@ -1183,6 +1283,45 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
setDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full my-2"
|
||||
placeholder={t("Tag") + " (" + t("Optional") + ")"}
|
||||
onChange={(e) => {
|
||||
setTag(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<div className="join w-full">
|
||||
<input
|
||||
type="number"
|
||||
className="input flex-1 join-item"
|
||||
placeholder={t("File Size") + " (" + t("Optional") + ")"}
|
||||
value={fileSize}
|
||||
onChange={(e) => {
|
||||
setFileSize(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<select
|
||||
className="select w-24 join-item"
|
||||
value={fileSizeUnit}
|
||||
onChange={(e) => {
|
||||
setFileSizeUnit(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="B">B</option>
|
||||
<option value="KB">KB</option>
|
||||
<option value="MB">MB</option>
|
||||
<option value="GB">GB</option>
|
||||
</select>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full my-2"
|
||||
placeholder={"MD5" + " (" + t("Optional") + ")"}
|
||||
onChange={(e) => {
|
||||
setMd5(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1239,6 +1378,14 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
setDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full my-2"
|
||||
placeholder={t("Tag") + " (" + t("Optional") + ")"}
|
||||
onChange={(e) => {
|
||||
setTag(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1311,6 +1458,14 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||
setDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full my-2"
|
||||
placeholder={t("Tag") + " (" + t("Optional") + ")"}
|
||||
onChange={(e) => {
|
||||
setTag(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1340,6 +1495,8 @@ function UpdateFileInfoDialog({ file }: { file: RFile }) {
|
||||
|
||||
const [description, setDescription] = useState(file.description);
|
||||
|
||||
const [tag, setTag] = useState(file.tag || "");
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const reload = useContext(context);
|
||||
@@ -1349,7 +1506,7 @@ function UpdateFileInfoDialog({ file }: { file: RFile }) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await network.updateFile(file.id, filename, description);
|
||||
const res = await network.updateFile(file.id, filename, description, tag);
|
||||
const dialog = document.getElementById(
|
||||
`update_file_info_dialog_${file.id}`,
|
||||
) as HTMLDialogElement;
|
||||
@@ -1397,6 +1554,12 @@ function UpdateFileInfoDialog({ file }: { file: RFile }) {
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type={"text"}
|
||||
label={t("Tag") + " (" + t("Optional") + ")"}
|
||||
value={tag}
|
||||
onChange={(e) => setTag(e.target.value)}
|
||||
/>
|
||||
<div className="modal-action">
|
||||
<form method="dialog">
|
||||
<button className="btn btn-ghost">{t("Close")}</button>
|
||||
|
||||
@@ -8,8 +8,8 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:3000",
|
||||
// target: "https://res.nyne.dev",
|
||||
// target: "http://localhost:3000",
|
||||
target: "https://nysoure.com",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"https://www.moyu.moe": {
|
||||
|
||||
3
go.mod
3
go.mod
@@ -14,6 +14,7 @@ require (
|
||||
github.com/blevesearch/bleve v1.0.14
|
||||
github.com/chai2010/webp v1.4.0
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/redis/go-redis/v9 v9.17.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
)
|
||||
@@ -30,8 +31,10 @@ require (
|
||||
github.com/blevesearch/zap/v13 v13.0.6 // indirect
|
||||
github.com/blevesearch/zap/v14 v14.0.5 // indirect
|
||||
github.com/blevesearch/zap/v15 v15.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/couchbase/vellum v1.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/golang/protobuf v1.3.2 // indirect
|
||||
|
||||
10
go.sum
10
go.sum
@@ -29,6 +29,12 @@ github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67n
|
||||
github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY=
|
||||
github.com/blevesearch/zap/v15 v15.0.3 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY=
|
||||
github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
|
||||
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
@@ -46,6 +52,8 @@ github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
@@ -135,6 +143,8 @@ github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJ
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/redis/go-redis/v9 v9.17.0 h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM=
|
||||
github.com/redis/go-redis/v9 v9.17.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
|
||||
3
main.go
3
main.go
@@ -38,7 +38,8 @@ func main() {
|
||||
api.AddCommentRoutes(apiG)
|
||||
api.AddConfigRoutes(apiG)
|
||||
api.AddActivityRoutes(apiG)
|
||||
api.AddCollectionRoutes(apiG) // 新增
|
||||
api.AddCollectionRoutes(apiG)
|
||||
api.AddProxyRoutes(apiG)
|
||||
}
|
||||
|
||||
log.Fatal(app.Listen(":3000"))
|
||||
|
||||
@@ -43,6 +43,7 @@ func initUpload(c fiber.Ctx) error {
|
||||
FileSize int64 `json:"file_size"`
|
||||
ResourceID uint `json:"resource_id"`
|
||||
StorageID uint `json:"storage_id"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
var req InitUploadRequest
|
||||
@@ -50,7 +51,10 @@ func initUpload(c fiber.Ctx) error {
|
||||
return model.NewRequestError("Invalid request parameters")
|
||||
}
|
||||
|
||||
result, err := service.CreateUploadingFile(uid, req.Filename, req.Description, req.FileSize, req.ResourceID, req.StorageID)
|
||||
req.Filename = strings.TrimSpace(req.Filename)
|
||||
req.Tag = strings.TrimSpace(req.Tag)
|
||||
|
||||
result, err := service.CreateUploadingFile(uid, req.Filename, req.Description, req.FileSize, req.ResourceID, req.StorageID, req.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -136,6 +140,9 @@ func createRedirectFile(c fiber.Ctx) error {
|
||||
Description string `json:"description"`
|
||||
ResourceID uint `json:"resource_id"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
Md5 string `json:"md5"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
var req CreateRedirectFileRequest
|
||||
@@ -143,7 +150,11 @@ func createRedirectFile(c fiber.Ctx) error {
|
||||
return model.NewRequestError("Invalid request parameters")
|
||||
}
|
||||
|
||||
result, err := service.CreateRedirectFile(uid, req.Filename, req.Description, req.ResourceID, req.RedirectURL)
|
||||
req.Filename = strings.TrimSpace(req.Filename)
|
||||
req.Md5 = strings.TrimSpace(req.Md5)
|
||||
req.Tag = strings.TrimSpace(req.Tag)
|
||||
|
||||
result, err := service.CreateRedirectFile(uid, req.Filename, req.Description, req.ResourceID, req.RedirectURL, req.FileSize, req.Md5, req.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -172,6 +183,7 @@ func updateFile(c fiber.Ctx) error {
|
||||
type UpdateFileRequest struct {
|
||||
Filename string `json:"filename"`
|
||||
Description string `json:"description"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
var req UpdateFileRequest
|
||||
@@ -179,7 +191,10 @@ func updateFile(c fiber.Ctx) error {
|
||||
return model.NewRequestError("Invalid request parameters")
|
||||
}
|
||||
|
||||
result, err := service.UpdateFile(uid, c.Params("id"), req.Filename, req.Description)
|
||||
req.Filename = strings.TrimSpace(req.Filename)
|
||||
req.Tag = strings.TrimSpace(req.Tag)
|
||||
|
||||
result, err := service.UpdateFile(uid, c.Params("id"), req.Filename, req.Description, req.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -259,13 +274,18 @@ func createServerDownloadTask(c fiber.Ctx) error {
|
||||
Description string `json:"description"`
|
||||
ResourceID uint `json:"resource_id"`
|
||||
StorageID uint `json:"storage_id"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
var req InitUploadRequest
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return model.NewRequestError("Invalid request parameters")
|
||||
}
|
||||
result, err := service.CreateServerDownloadTask(uid, req.Url, req.Filename, req.Description, req.ResourceID, req.StorageID)
|
||||
|
||||
req.Filename = strings.TrimSpace(req.Filename)
|
||||
req.Tag = strings.TrimSpace(req.Tag)
|
||||
|
||||
result, err := service.CreateServerDownloadTask(uid, req.Url, req.Filename, req.Description, req.ResourceID, req.StorageID, req.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
127
server/api/proxy.go
Normal file
127
server/api/proxy.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"nysoure/server/cache"
|
||||
"nysoure/server/model"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
allowedUrlRegexps []*regexp.Regexp
|
||||
)
|
||||
|
||||
type proxyResponse struct {
|
||||
Content string `json:"content"`
|
||||
ContentType string `json:"content_type"`
|
||||
StatusCode int `json:"status_code"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
regexps := os.Getenv("ALLOWED_URL_REGEXPS")
|
||||
for _, expr := range strings.Split(regexps, ",") {
|
||||
if expr == "" {
|
||||
continue
|
||||
}
|
||||
re, err := regexp.Compile(expr)
|
||||
if err != nil {
|
||||
panic("Invalid regex in ALLOWED_URL_REGEXPS: " + expr)
|
||||
}
|
||||
allowedUrlRegexps = append(allowedUrlRegexps, re)
|
||||
}
|
||||
}
|
||||
|
||||
func handleProxyCall(c fiber.Ctx) error {
|
||||
uriBase64 := c.Query("uri")
|
||||
if uriBase64 == "" {
|
||||
return model.NewRequestError("Missing uri parameter")
|
||||
}
|
||||
uriStr, err := base64.URLEncoding.DecodeString(uriBase64)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid base64 encoding")
|
||||
}
|
||||
uri, err := url.Parse(string(uriStr))
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid URL")
|
||||
}
|
||||
allowed := false
|
||||
for _, re := range allowedUrlRegexps {
|
||||
if re.MatchString(uri.String()) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return model.NewRequestError("URL not allowed")
|
||||
}
|
||||
|
||||
var resp *proxyResponse
|
||||
|
||||
rawVal, err := cache.Get("proxy:" + uri.String())
|
||||
if err == nil {
|
||||
var r proxyResponse
|
||||
err = json.Unmarshal([]byte(rawVal), &r)
|
||||
if err != nil {
|
||||
slog.ErrorContext(c, "Failed to unmarshal cached proxy response", "error", err)
|
||||
return model.NewInternalServerError("Error")
|
||||
}
|
||||
resp = &r
|
||||
} else {
|
||||
resp, err = proxy(uri)
|
||||
if err != nil {
|
||||
slog.ErrorContext(c, "Proxy request failed", "error", err)
|
||||
return model.NewInternalServerError("Error")
|
||||
}
|
||||
}
|
||||
|
||||
c.Status(resp.StatusCode)
|
||||
c.Response().Header.SetContentType(resp.ContentType)
|
||||
return c.SendString(resp.Content)
|
||||
}
|
||||
|
||||
func proxy(uri *url.URL) (*proxyResponse, error) {
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest("GET", uri.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxyResp := &proxyResponse{
|
||||
Content: string(data),
|
||||
ContentType: contentType,
|
||||
StatusCode: resp.StatusCode,
|
||||
}
|
||||
|
||||
j, err := json.Marshal(proxyResp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = cache.Set("proxy:"+uri.String(), string(j), 24*time.Hour)
|
||||
if err != nil {
|
||||
slog.Error("Failed to cache proxy response", "error", err)
|
||||
}
|
||||
return proxyResp, nil
|
||||
}
|
||||
|
||||
func AddProxyRoutes(router fiber.Router) {
|
||||
router.Get("/proxy", handleProxyCall)
|
||||
}
|
||||
42
server/cache/cache.go
vendored
Normal file
42
server/cache/cache.go
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var (
|
||||
client *redis.Client
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
func init() {
|
||||
host := os.Getenv("REDIS_HOST")
|
||||
port := os.Getenv("REDIS_PORT")
|
||||
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
if port == "" {
|
||||
port = "6379"
|
||||
}
|
||||
|
||||
client = redis.NewClient(&redis.Options{
|
||||
Addr: host + ":" + port,
|
||||
})
|
||||
}
|
||||
|
||||
func Get(key string) (string, error) {
|
||||
val, err := client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func Set(key, value string, expiration time.Duration) error {
|
||||
return client.Set(ctx, key, value, expiration).Err()
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func CreateUploadingFile(filename string, description string, fileSize int64, blockSize int64, tempPath string, resourceID, storageID, userID uint) (*model.UploadingFile, error) {
|
||||
func CreateUploadingFile(filename string, description string, fileSize int64, blockSize int64, tempPath string, resourceID, storageID, userID uint, tag string) (*model.UploadingFile, error) {
|
||||
blocksCount := (fileSize + blockSize - 1) / blockSize
|
||||
uf := &model.UploadingFile{
|
||||
Filename: filename,
|
||||
@@ -22,6 +22,7 @@ func CreateUploadingFile(filename string, description string, fileSize int64, bl
|
||||
TargetResourceID: resourceID,
|
||||
TargetStorageID: storageID,
|
||||
UserID: userID,
|
||||
Tag: tag,
|
||||
}
|
||||
if err := db.Create(uf).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -73,7 +74,7 @@ func GetUploadingFilesOlderThan(time time.Time) ([]model.UploadingFile, error) {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func CreateFile(filename string, description string, resourceID uint, storageID *uint, storageKey string, redirectUrl string, size int64, userID uint, hash string) (*model.File, error) {
|
||||
func CreateFile(filename string, description string, resourceID uint, storageID *uint, storageKey string, redirectUrl string, size int64, userID uint, hash string, tag string) (*model.File, error) {
|
||||
if storageID == nil && redirectUrl == "" {
|
||||
return nil, errors.New("storageID and redirectUrl cannot be both empty")
|
||||
}
|
||||
@@ -89,6 +90,7 @@ func CreateFile(filename string, description string, resourceID uint, storageID
|
||||
Size: size,
|
||||
UserID: userID,
|
||||
Hash: hash,
|
||||
Tag: tag,
|
||||
}
|
||||
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
@@ -171,7 +173,7 @@ func DeleteFile(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateFile(id string, filename string, description string) (*model.File, error) {
|
||||
func UpdateFile(id string, filename string, description string, tag string) (*model.File, error) {
|
||||
f := &model.File{}
|
||||
if err := db.Where("uuid = ?", id).First(f).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -182,6 +184,9 @@ func UpdateFile(id string, filename string, description string) (*model.File, er
|
||||
if description != "" {
|
||||
f.Description = description
|
||||
}
|
||||
if tag != "" {
|
||||
f.Tag = tag
|
||||
}
|
||||
if err := db.Save(f).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, model.NewNotFoundError("file not found")
|
||||
|
||||
@@ -19,6 +19,7 @@ type File struct {
|
||||
User User `gorm:"foreignKey:UserID"`
|
||||
Size int64
|
||||
Hash string `gorm:"default:null"`
|
||||
Tag string `gorm:"type:text;default:null"`
|
||||
}
|
||||
|
||||
type FileView struct {
|
||||
@@ -32,6 +33,7 @@ type FileView struct {
|
||||
Hash string `json:"hash,omitempty"`
|
||||
StorageName string `json:"storage_name,omitempty"`
|
||||
CreatedAt int64 `json:"created_at,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
}
|
||||
|
||||
func (f *File) ToView() *FileView {
|
||||
@@ -45,6 +47,7 @@ func (f *File) ToView() *FileView {
|
||||
Hash: f.Hash,
|
||||
StorageName: f.Storage.Name,
|
||||
CreatedAt: f.CreatedAt.Unix(),
|
||||
Tag: f.Tag,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,5 +67,6 @@ func (f *File) ToViewWithResource() *FileView {
|
||||
User: f.User.ToView(),
|
||||
Resource: resource,
|
||||
Hash: f.Hash,
|
||||
Tag: f.Tag,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type UploadingFile struct {
|
||||
TempPath string
|
||||
Resource Resource `gorm:"foreignKey:TargetResourceID"`
|
||||
Storage Storage `gorm:"foreignKey:TargetStorageID"`
|
||||
Tag string `gorm:"type:text;default:null"`
|
||||
}
|
||||
|
||||
func (uf *UploadingFile) BlocksCount() int {
|
||||
|
||||
@@ -80,7 +80,7 @@ func init() {
|
||||
}()
|
||||
}
|
||||
|
||||
func CreateUploadingFile(uid uint, filename string, description string, fileSize int64, resourceID, storageID uint) (*model.UploadingFileView, error) {
|
||||
func CreateUploadingFile(uid uint, filename string, description string, fileSize int64, resourceID, storageID uint, tag string) (*model.UploadingFileView, error) {
|
||||
if filename == "" {
|
||||
return nil, model.NewRequestError("filename is empty")
|
||||
}
|
||||
@@ -113,7 +113,7 @@ func CreateUploadingFile(uid uint, filename string, description string, fileSize
|
||||
log.Error("failed to create temp dir: ", err)
|
||||
return nil, model.NewInternalServerError("failed to create temp dir")
|
||||
}
|
||||
uploadingFile, err := dao.CreateUploadingFile(filename, description, fileSize, blockSize, tempPath, resourceID, storageID, uid)
|
||||
uploadingFile, err := dao.CreateUploadingFile(filename, description, fileSize, blockSize, tempPath, resourceID, storageID, uid, tag)
|
||||
if err != nil {
|
||||
log.Error("failed to create uploading file: ", err)
|
||||
_ = os.Remove(tempPath)
|
||||
@@ -245,7 +245,7 @@ func FinishUploadingFile(uid uint, fid uint, md5Str string) (*model.FileView, er
|
||||
return nil, model.NewInternalServerError("failed to finish uploading file. please re-upload")
|
||||
}
|
||||
|
||||
dbFile, err := dao.CreateFile(uploadingFile.Filename, uploadingFile.Description, uploadingFile.TargetResourceID, &uploadingFile.TargetStorageID, storageKeyUnavailable, "", uploadingFile.TotalSize, uid, sumStr)
|
||||
dbFile, err := dao.CreateFile(uploadingFile.Filename, uploadingFile.Description, uploadingFile.TargetResourceID, &uploadingFile.TargetStorageID, storageKeyUnavailable, "", uploadingFile.TotalSize, uid, sumStr, uploadingFile.Tag)
|
||||
if err != nil {
|
||||
log.Error("failed to create file in db: ", err)
|
||||
_ = os.Remove(resultFilePath)
|
||||
@@ -309,7 +309,7 @@ func CancelUploadingFile(uid uint, fid uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateRedirectFile(uid uint, filename string, description string, resourceID uint, redirectUrl string) (*model.FileView, error) {
|
||||
func CreateRedirectFile(uid uint, filename string, description string, resourceID uint, redirectUrl string, fileSize int64, md5 string, tag string) (*model.FileView, error) {
|
||||
u, err := url.Parse(redirectUrl)
|
||||
if err != nil {
|
||||
return nil, model.NewRequestError("URL is not valid")
|
||||
@@ -329,7 +329,7 @@ func CreateRedirectFile(uid uint, filename string, description string, resourceI
|
||||
return nil, model.NewUnAuthorizedError("user cannot upload file")
|
||||
}
|
||||
|
||||
file, err := dao.CreateFile(filename, description, resourceID, nil, "", redirectUrl, 0, uid, "")
|
||||
file, err := dao.CreateFile(filename, description, resourceID, nil, "", redirectUrl, fileSize, uid, md5, tag)
|
||||
if err != nil {
|
||||
log.Error("failed to create file in db: ", err)
|
||||
return nil, model.NewInternalServerError("failed to create file in db")
|
||||
@@ -373,7 +373,7 @@ func DeleteFile(uid uint, fid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateFile(uid uint, fid string, filename string, description string) (*model.FileView, error) {
|
||||
func UpdateFile(uid uint, fid string, filename string, description string, tag string) (*model.FileView, error) {
|
||||
file, err := dao.GetFile(fid)
|
||||
if err != nil {
|
||||
log.Error("failed to get file: ", err)
|
||||
@@ -390,7 +390,7 @@ func UpdateFile(uid uint, fid string, filename string, description string) (*mod
|
||||
return nil, model.NewUnAuthorizedError("user cannot update file")
|
||||
}
|
||||
|
||||
file, err = dao.UpdateFile(fid, filename, description)
|
||||
file, err = dao.UpdateFile(fid, filename, description, tag)
|
||||
if err != nil {
|
||||
log.Error("failed to update file in db: ", err)
|
||||
return nil, model.NewInternalServerError("failed to update file in db")
|
||||
@@ -575,7 +575,7 @@ func downloadFile(ctx context.Context, url string, path string) (string, error)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateServerDownloadTask(uid uint, url, filename, description string, resourceID, storageID uint) (*model.FileView, error) {
|
||||
func CreateServerDownloadTask(uid uint, url, filename, description string, resourceID, storageID uint, tag string) (*model.FileView, error) {
|
||||
canUpload, err := checkUserCanUpload(uid)
|
||||
if err != nil {
|
||||
log.Error("failed to check user permission: ", err)
|
||||
@@ -596,7 +596,7 @@ func CreateServerDownloadTask(uid uint, url, filename, description string, resou
|
||||
return nil, model.NewRequestError("server is busy, please try again later")
|
||||
}
|
||||
|
||||
file, err := dao.CreateFile(filename, description, resourceID, &storageID, storageKeyUnavailable, "", 0, uid, "")
|
||||
file, err := dao.CreateFile(filename, description, resourceID, &storageID, storageKeyUnavailable, "", 0, uid, "", tag)
|
||||
if err != nil {
|
||||
log.Error("failed to create file in db: ", err)
|
||||
return nil, model.NewInternalServerError("failed to create file in db")
|
||||
|
||||
Reference in New Issue
Block a user