diff --git a/config.ts b/config.ts index fecf356..9979953 100644 --- a/config.ts +++ b/config.ts @@ -28,6 +28,7 @@ export type ConfigType = { flutter_frontend?: string; fetch_timeout: number; download_timeout: number; + ffprobe_path: string; }; export enum ThumbnailMethod { @@ -172,6 +173,9 @@ export class Config { get download_timeout() { return this._return_number("download_timeout") || 10000; } + get ffprobe_path() { + return this._return_string("ffprobe_path") || "ffprobe"; + } to_json(): ConfigType { return { cookies: typeof this.cookies === "string", @@ -200,6 +204,7 @@ export class Config { flutter_frontend: this.flutter_frontend, fetch_timeout: this.fetch_timeout, download_timeout: this.download_timeout, + ffprobe_path: this.ffprobe_path, }; } } diff --git a/fresh.gen.ts b/fresh.gen.ts index 1a8a945..c64d2e5 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -10,27 +10,30 @@ import * as $4 from "./routes/api/exit.ts"; import * as $5 from "./routes/api/export/gallery/zip/[gid].ts"; import * as $6 from "./routes/api/file/[id].ts"; import * as $7 from "./routes/api/file/random.ts"; -import * as $8 from "./routes/api/filemeta.ts"; -import * as $9 from "./routes/api/filemeta/[token].ts"; -import * as $10 from "./routes/api/files/[token].ts"; -import * as $11 from "./routes/api/gallery/[gid].ts"; -import * as $12 from "./routes/api/gallery/list.ts"; -import * as $13 from "./routes/api/status.ts"; -import * as $14 from "./routes/api/tag/[id].ts"; -import * as $15 from "./routes/api/tag/rows.ts"; -import * as $16 from "./routes/api/task.ts"; -import * as $17 from "./routes/api/thumbnail/[id].ts"; -import * as $18 from "./routes/api/token.ts"; -import * as $19 from "./routes/api/user.ts"; -import * as $20 from "./routes/file/[id].ts"; -import * as $21 from "./routes/file/_middleware.ts"; -import * as $22 from "./routes/index.tsx"; -import * as $23 from "./routes/manifest.json.ts"; -import * as $24 from "./routes/thumbnail/[id].ts"; -import * as $25 from "./routes/thumbnail/_middleware.ts"; +import * as $8 from "./routes/api/file/upload.ts"; +import * as $9 from "./routes/api/filemeta.ts"; +import * as $10 from "./routes/api/filemeta/[token].ts"; +import * as $11 from "./routes/api/files/[token].ts"; +import * as $12 from "./routes/api/gallery/[gid].ts"; +import * as $13 from "./routes/api/gallery/list.ts"; +import * as $14 from "./routes/api/status.ts"; +import * as $15 from "./routes/api/tag/[id].ts"; +import * as $16 from "./routes/api/tag/rows.ts"; +import * as $17 from "./routes/api/task.ts"; +import * as $18 from "./routes/api/thumbnail/[id].ts"; +import * as $19 from "./routes/api/token.ts"; +import * as $20 from "./routes/api/user.ts"; +import * as $21 from "./routes/file/[id].ts"; +import * as $22 from "./routes/file/_middleware.ts"; +import * as $23 from "./routes/index.tsx"; +import * as $24 from "./routes/manifest.json.ts"; +import * as $25 from "./routes/thumbnail/[id].ts"; +import * as $26 from "./routes/thumbnail/_middleware.ts"; +import * as $27 from "./routes/upload.tsx"; import * as $$0 from "./islands/Container.tsx"; import * as $$1 from "./islands/Settings.tsx"; import * as $$2 from "./islands/TaskManager.tsx"; +import * as $$3 from "./islands/Upload.tsx"; const manifest = { routes: { @@ -42,29 +45,32 @@ const manifest = { "./routes/api/export/gallery/zip/[gid].ts": $5, "./routes/api/file/[id].ts": $6, "./routes/api/file/random.ts": $7, - "./routes/api/filemeta.ts": $8, - "./routes/api/filemeta/[token].ts": $9, - "./routes/api/files/[token].ts": $10, - "./routes/api/gallery/[gid].ts": $11, - "./routes/api/gallery/list.ts": $12, - "./routes/api/status.ts": $13, - "./routes/api/tag/[id].ts": $14, - "./routes/api/tag/rows.ts": $15, - "./routes/api/task.ts": $16, - "./routes/api/thumbnail/[id].ts": $17, - "./routes/api/token.ts": $18, - "./routes/api/user.ts": $19, - "./routes/file/[id].ts": $20, - "./routes/file/_middleware.ts": $21, - "./routes/index.tsx": $22, - "./routes/manifest.json.ts": $23, - "./routes/thumbnail/[id].ts": $24, - "./routes/thumbnail/_middleware.ts": $25, + "./routes/api/file/upload.ts": $8, + "./routes/api/filemeta.ts": $9, + "./routes/api/filemeta/[token].ts": $10, + "./routes/api/files/[token].ts": $11, + "./routes/api/gallery/[gid].ts": $12, + "./routes/api/gallery/list.ts": $13, + "./routes/api/status.ts": $14, + "./routes/api/tag/[id].ts": $15, + "./routes/api/tag/rows.ts": $16, + "./routes/api/task.ts": $17, + "./routes/api/thumbnail/[id].ts": $18, + "./routes/api/token.ts": $19, + "./routes/api/user.ts": $20, + "./routes/file/[id].ts": $21, + "./routes/file/_middleware.ts": $22, + "./routes/index.tsx": $23, + "./routes/manifest.json.ts": $24, + "./routes/thumbnail/[id].ts": $25, + "./routes/thumbnail/_middleware.ts": $26, + "./routes/upload.tsx": $27, }, islands: { "./islands/Container.tsx": $$0, "./islands/Settings.tsx": $$1, "./islands/TaskManager.tsx": $$2, + "./islands/Upload.tsx": $$3, }, baseUrl: import.meta.url, }; diff --git a/islands/Upload.tsx b/islands/Upload.tsx new file mode 100644 index 0000000..e85a845 --- /dev/null +++ b/islands/Upload.tsx @@ -0,0 +1,44 @@ +import t, { i18n_map, I18NMap } from "../server/i18n.ts"; + +export type UploaderProps = { + i18n?: I18NMap; + lang?: string; +}; + +export default function Uploader(props: UploaderProps) { + if (props.i18n) i18n_map.value = props.i18n; + return ( +
+ +
+ {t("upload.filename")}{" "} + +
+ {" "} + +
+ +
+ +
+ ); +} diff --git a/routes/api/file/upload.ts b/routes/api/file/upload.ts new file mode 100644 index 0000000..dbcbca1 --- /dev/null +++ b/routes/api/file/upload.ts @@ -0,0 +1,75 @@ +import { Handlers } from "$fresh/server.ts"; +import type { EhFile } from "../../../db.ts"; +import { get_task_manager } from "../../../server.ts"; +import { return_data, return_error } from "../../../server/utils.ts"; +import { get_string, parse_bool } from "../../../server/parse_form.ts"; +import { fb_get_size } from "../../../thumbnail/ffmpeg_binary.ts"; +import { sure_dir } from "../../../utils.ts"; +import mime from "mime"; +import { extname, join, resolve } from "std/path/mod.ts"; + +export const handler: Handlers = { + async POST(req, _ctx) { + const m = get_task_manager(); + try { + const form = await req.formData(); + const file = form.get("file"); + if (!file) { + return return_error(1, "Missing file."); + } + const mext = typeof file === "string" + ? null + : `.${mime.getExtension(file.type)}`; + const filename = (await get_string(form.get("filename"))) || + (typeof file === "string" ? null : file.name); + if (!filename) { + return return_error(2, "Missing filename."); + } + const fext = extname(filename); + const fn = mext == fext + ? filename + : `${filename.slice(0, filename.length - fext.length)}${mext}`; + const dir = (await get_string(form.get("dir"))) || + join(m.cfg.base, "uploaded"); + const is_original = await parse_bool( + form.get("is_original"), + false, + ); + const token = await get_string(form.get("token")); + if (!token) { + return return_error(3, "Missing token."); + } + const path = join(dir, fn); + await sure_dir(dir); + try { + if (typeof file === "string") { + await Deno.writeTextFile(path, file); + } else { + await Deno.writeFile(path, file.stream()); + } + const size = await fb_get_size(path); + if (!size) { + await Deno.remove(path); + return return_error(4, "Failed to get file size."); + } + const rpath = resolve(path); + const f = { + id: 0, + path: rpath, + width: size.width, + height: size.height, + is_original, + token, + } as EhFile; + const nf = m.db.add_file(f, false); + return return_data(nf); + } catch (e) { + await Deno.remove(path); + throw e; + } + } catch (e) { + console.error(e); + return return_error(500, "Internal Server Error."); + } + }, +}; diff --git a/routes/api/status.ts b/routes/api/status.ts index 4be2e9e..fcb0f83 100644 --- a/routes/api/status.ts +++ b/routes/api/status.ts @@ -26,6 +26,9 @@ export const handler: Handlers = { m.cfg.ffmpeg_path, ); const ffmpeg_api_enabled = await check_ffmpeg_api(); + const ffprobe_binary_enabled = await check_ffmpeg_binary( + m.cfg.ffprobe_path, + ); const meilisearch_enabled = m.meilisearch !== undefined; let meilisearch; if ( @@ -50,6 +53,7 @@ export const handler: Handlers = { return return_data({ ffmpeg_api_enabled, ffmpeg_binary_enabled, + ffprobe_binary_enabled, meilisearch_enabled, meilisearch, no_user, diff --git a/routes/upload.tsx b/routes/upload.tsx new file mode 100644 index 0000000..314e2f8 --- /dev/null +++ b/routes/upload.tsx @@ -0,0 +1,31 @@ +import { Handlers, PageProps } from "$fresh/server.ts"; +import GlobalContext from "../components/GlobalContext.tsx"; +import Uploader from "../islands/Upload.tsx"; +import { get_i18nmap, i18n_handle_request } from "../server/i18ns.ts"; + +type Props = { + lang: string; +}; + +export const handler: Handlers = { + GET(req, ctx) { + const re = i18n_handle_request(req); + if (typeof re === "string") { + return ctx.render({ + lang: re, + }); + } + return re; + }, +}; + +export default function Upload({ data }: PageProps) { + const i18n = get_i18nmap(data.lang, "upload"); + return ( + + + + + + ); +} diff --git a/server/i18ns.ts b/server/i18ns.ts index 6b4065a..1df3744 100644 --- a/server/i18ns.ts +++ b/server/i18ns.ts @@ -7,8 +7,8 @@ import { get_host } from "./utils.ts"; const whole_maps = new Map(); const LANGUAGES = ["zh-cn"]; -type MODULE = "common" | "settings" | "task" | "user"; -const MODULES: MODULE[] = ["common", "settings", "task", "user"]; +type MODULE = "common" | "settings" | "task" | "upload" | "user"; +const MODULES: MODULE[] = ["common", "settings", "task", "upload", "user"]; export async function load_translation(signal?: AbortSignal) { let base = import.meta.resolve("../translation").slice(7); diff --git a/server/status.ts b/server/status.ts index 5e7224e..255d227 100644 --- a/server/status.ts +++ b/server/status.ts @@ -1,6 +1,7 @@ export type StatusData = { ffmpeg_api_enabled: boolean; ffmpeg_binary_enabled: boolean; + ffprobe_binary_enabled: boolean; meilisearch_enabled: boolean; meilisearch?: { host: string; diff --git a/tasks/download.ts b/tasks/download.ts index a11d517..9e2ff4d 100644 --- a/tasks/download.ts +++ b/tasks/download.ts @@ -11,6 +11,7 @@ import { import { RecoverableError, TaskManager } from "../task_manager.ts"; import { add_suffix_to_path, + asyncEvery, asyncFilter, promiseState, PromiseStatus, @@ -235,11 +236,13 @@ export async function download_task( async function download_task(names: Record, i: Image) { const ofiles = db.get_files(i.page_token); if (ofiles.length) { - const t = ofiles[0]; - if ( - (t.is_original || !download_original_img) && - (await exists(t.path)) - ) { + const need = await asyncEvery( + ofiles, + async (t) => + (!t.is_original && download_original_img) || + (!await exists(t.path)), + ); + if (!need) { const p = db.get_pmeta_by_index(task.gid, i.index); if (!p) { const op = db.get_pmeta_by_token( diff --git a/thumbnail/ffmpeg_binary.ts b/thumbnail/ffmpeg_binary.ts index 663ea13..b191979 100644 --- a/thumbnail/ffmpeg_binary.ts +++ b/thumbnail/ffmpeg_binary.ts @@ -11,6 +11,34 @@ export async function check_ffmpeg_binary(p: string) { return o.code === 0; } +export async function fb_get_size(i: string) { + const cmd = new Deno.Command("ffprobe", { + stdout: "piped", + stderr: "piped", + args: [ + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "stream=width,height", + "-of", + "csv=s=x:p=0", + i, + ], + }); + const c = cmd.spawn(); + const o = await c.output(); + if (o.code !== 0) { + return null; + } + const s = (new TextDecoder()).decode(o.stdout).trim().split("x"); + return { + width: parseInt(s[0]), + height: parseInt(s[1]), + }; +} + export async function fb_generate_thumbnail( p: string, i: string, diff --git a/translation/en/upload.jsonc b/translation/en/upload.jsonc new file mode 100644 index 0000000..267dd6c --- /dev/null +++ b/translation/en/upload.jsonc @@ -0,0 +1,6 @@ +{ + "upload": "Upload file", + "filename": "Filename", + "is_original": "Marked as original file", + "token": "Token" +} diff --git a/translation/zh-cn/upload.jsonc b/translation/zh-cn/upload.jsonc new file mode 100644 index 0000000..0d7fbc6 --- /dev/null +++ b/translation/zh-cn/upload.jsonc @@ -0,0 +1,6 @@ +{ + "upload": "上传文件", + "filename": "文件名", + "is_original": "标记为原始文件", + "token": "Token" +}