diff --git a/packages/cloudscript/local.ts b/packages/cloudscript/local.ts index b21d015ed..dc0e3655c 100644 --- a/packages/cloudscript/local.ts +++ b/packages/cloudscript/local.ts @@ -1,7 +1,7 @@ import { ExtVersion } from "@App/app/const"; import type { Script } from "@App/app/repo/scripts"; import type { Value } from "@App/app/repo/value"; -import type JSZip from "jszip"; +import { type JSZipFile } from "@App/pkg/utils/jszip-x"; import packageTpl from "@App/template/cloudcat-package/package.tpl"; import utilsTpl from "@App/template/cloudcat-package/utils.tpl"; import indexTpl from "@App/template/cloudcat-package/index.tpl"; @@ -10,12 +10,12 @@ import type CloudScript from "./cloudscript"; // 导出到本地,一个可执行到npm包 export default class LocalCloudScript implements CloudScript { - zip: JSZip; + zip: JSZipFile; params: ExportParams; constructor(params: ExportParams) { - this.zip = params.zip! as JSZip; + this.zip = params.zip! as JSZipFile; this.params = params; } diff --git a/packages/filesystem/baidu/baidu.ts b/packages/filesystem/baidu/baidu.ts index 26614fafb..88f013150 100644 --- a/packages/filesystem/baidu/baidu.ts +++ b/packages/filesystem/baidu/baidu.ts @@ -1,6 +1,6 @@ import { AuthVerify } from "../auth"; import type FileSystem from "../filesystem"; -import type { File, FileReader, FileWriter } from "../filesystem"; +import type { File, FileCreateOptions, FileReader, FileWriter } from "../filesystem"; import { joinPath } from "../utils"; import { BaiduFileReader, BaiduFileWriter } from "./rw"; @@ -29,11 +29,11 @@ export default class BaiduFileSystem implements FileSystem { return new BaiduFileSystem(joinPath(this.path, path), this.accessToken); } - async create(path: string): Promise { + async create(path: string, _opts?: FileCreateOptions): Promise { return new BaiduFileWriter(this, joinPath(this.path, path)); } - async createDir(dir: string): Promise { + async createDir(dir: string, _opts?: FileCreateOptions): Promise { dir = joinPath(this.path, dir); const urlencoded = new URLSearchParams(); urlencoded.append("path", dir); diff --git a/packages/filesystem/dropbox/dropbox.ts b/packages/filesystem/dropbox/dropbox.ts index 0bae19c96..79b97a57a 100644 --- a/packages/filesystem/dropbox/dropbox.ts +++ b/packages/filesystem/dropbox/dropbox.ts @@ -1,6 +1,6 @@ import { AuthVerify } from "../auth"; import type FileSystem from "../filesystem"; -import type { File, FileReader, FileWriter } from "../filesystem"; +import type { File, FileCreateOptions, FileReader, FileWriter } from "../filesystem"; import { joinPath } from "../utils"; import { DropboxFileReader, DropboxFileWriter } from "./rw"; @@ -32,11 +32,11 @@ export default class DropboxFileSystem implements FileSystem { return Promise.resolve(new DropboxFileSystem(joinPath(this.path, path), this.accessToken)); } - create(path: string): Promise { + create(path: string, _opts?: FileCreateOptions): Promise { return Promise.resolve(new DropboxFileWriter(this, joinPath(this.path, path))); } - async createDir(dir: string): Promise { + async createDir(dir: string, _opts?: FileCreateOptions): Promise { if (!dir) { return Promise.resolve(); } diff --git a/packages/filesystem/filesystem.ts b/packages/filesystem/filesystem.ts index 0ee1847ac..d5b74f77a 100644 --- a/packages/filesystem/filesystem.ts +++ b/packages/filesystem/filesystem.ts @@ -27,6 +27,10 @@ export interface FileWriter { export type FileReadWriter = FileReader & FileWriter; +export type FileCreateOptions = { + modifiedDate?: number; +}; + // 文件读取 export default interface FileSystem { // 授权验证 @@ -36,9 +40,9 @@ export default interface FileSystem { // 打开目录 openDir(path: string): Promise; // 创建文件 - create(path: string): Promise; + create(path: string, opts?: FileCreateOptions): Promise; // 创建目录 - createDir(dir: string): Promise; + createDir(dir: string, opts?: FileCreateOptions): Promise; // 删除文件 delete(path: string): Promise; // 文件列表 diff --git a/packages/filesystem/googledrive/googledrive.ts b/packages/filesystem/googledrive/googledrive.ts index f0b41ec4c..d9072fb66 100644 --- a/packages/filesystem/googledrive/googledrive.ts +++ b/packages/filesystem/googledrive/googledrive.ts @@ -1,6 +1,6 @@ import { AuthVerify } from "../auth"; import type FileSystem from "../filesystem"; -import type { File, FileReader, FileWriter } from "../filesystem"; +import type { File, FileCreateOptions, FileReader, FileWriter } from "../filesystem"; import { joinPath } from "../utils"; import { GoogleDriveFileReader, GoogleDriveFileWriter } from "./rw"; @@ -31,10 +31,10 @@ export default class GoogleDriveFileSystem implements FileSystem { return Promise.resolve(new GoogleDriveFileSystem(joinPath(this.path, path), this.accessToken)); } - create(path: string): Promise { + create(path: string, _opts?: FileCreateOptions): Promise { return Promise.resolve(new GoogleDriveFileWriter(this, joinPath(this.path, path))); } - async createDir(dir: string): Promise { + async createDir(dir: string, _opts?: FileCreateOptions): Promise { if (!dir) { return Promise.resolve(); } diff --git a/packages/filesystem/onedrive/onedrive.ts b/packages/filesystem/onedrive/onedrive.ts index 78d8bae1b..43005476d 100644 --- a/packages/filesystem/onedrive/onedrive.ts +++ b/packages/filesystem/onedrive/onedrive.ts @@ -1,5 +1,5 @@ import { AuthVerify } from "../auth"; -import type { File, FileReader, FileWriter } from "../filesystem"; +import type { File, FileCreateOptions, FileReader, FileWriter } from "../filesystem"; import type FileSystem from "../filesystem"; import { joinPath } from "../utils"; import { OneDriveFileReader, OneDriveFileWriter } from "./rw"; @@ -31,11 +31,11 @@ export default class OneDriveFileSystem implements FileSystem { return new OneDriveFileSystem(joinPath(this.path, path), this.accessToken); } - async create(path: string): Promise { + async create(path: string, _opts?: FileCreateOptions): Promise { return new OneDriveFileWriter(this, joinPath(this.path, path)); } - async createDir(dir: string): Promise { + async createDir(dir: string, _opts?: FileCreateOptions): Promise { if (dir && dir.startsWith("ScriptCat")) { dir = dir.substring(9); if (dir.startsWith("/")) { diff --git a/packages/filesystem/webdav/webdav.ts b/packages/filesystem/webdav/webdav.ts index f446533ec..11454fc38 100644 --- a/packages/filesystem/webdav/webdav.ts +++ b/packages/filesystem/webdav/webdav.ts @@ -1,7 +1,7 @@ import type { AuthType, FileStat, WebDAVClient } from "webdav"; import { createClient } from "webdav"; import type FileSystem from "../filesystem"; -import type { File, FileReader, FileWriter } from "../filesystem"; +import type { File, FileCreateOptions, FileReader, FileWriter } from "../filesystem"; import { joinPath } from "../utils"; import { WebDAVFileReader, WebDAVFileWriter } from "./rw"; import { WarpTokenError } from "../error"; @@ -47,11 +47,11 @@ export default class WebDAVFileSystem implements FileSystem { return new WebDAVFileSystem(this.client, joinPath(this.basePath, path), this.url); } - async create(path: string): Promise { + async create(path: string, _opts?: FileCreateOptions): Promise { return new WebDAVFileWriter(this.client, joinPath(this.basePath, path)); } - async createDir(path: string): Promise { + async createDir(path: string, _opts?: FileCreateOptions): Promise { try { await this.client.createDirectory(joinPath(this.basePath, path)); } catch (e: any) { diff --git a/packages/filesystem/zip/rw.ts b/packages/filesystem/zip/rw.ts index 1d022e73d..04bd30530 100644 --- a/packages/filesystem/zip/rw.ts +++ b/packages/filesystem/zip/rw.ts @@ -1,6 +1,6 @@ import type { JSZipObject } from "jszip"; -import type JSZip from "jszip"; -import type { FileReader, FileWriter } from "../filesystem"; +import type { JSZipFileOptions, JSZipFile } from "@App/pkg/utils/jszip-x"; +import type { FileCreateOptions, FileReader, FileWriter } from "../filesystem"; export class ZipFileReader implements FileReader { zipObject: JSZipObject; @@ -15,16 +15,27 @@ export class ZipFileReader implements FileReader { } export class ZipFileWriter implements FileWriter { - zip: JSZip; + zip: JSZipFile; path: string; - constructor(zip: JSZip, path: string) { + modifiedDate: number | undefined; + + constructor(zip: JSZipFile, path: string, opts?: FileCreateOptions) { this.zip = zip; this.path = path; + if (opts && opts.modifiedDate) { + this.modifiedDate = opts.modifiedDate; + } } async write(content: string): Promise { - this.zip.file(this.path, content); + const opts = {} as JSZipFileOptions; + if (this.modifiedDate) { + const date = new Date(this.modifiedDate); + const dateWithOffset = new Date(date.getTime() - date.getTimezoneOffset() * 60000); + opts.date = dateWithOffset; + } + this.zip.file(this.path, content, opts); } } diff --git a/packages/filesystem/zip/zip.ts b/packages/filesystem/zip/zip.ts index b182f8666..34356a3af 100644 --- a/packages/filesystem/zip/zip.ts +++ b/packages/filesystem/zip/zip.ts @@ -1,15 +1,15 @@ -import type JSZip from "jszip"; -import type { File, FileReader, FileWriter } from "@Packages/filesystem/filesystem"; +import { type JSZipFile } from "@App/pkg/utils/jszip-x"; +import type { File, FileCreateOptions, FileReader, FileWriter } from "@Packages/filesystem/filesystem"; import type FileSystem from "@Packages/filesystem/filesystem"; import { ZipFileReader, ZipFileWriter } from "./rw"; export default class ZipFileSystem implements FileSystem { - zip: JSZip; + zip: JSZipFile; basePath: string; // zip为空时,创建一个空的zip - constructor(zip: JSZip, basePath?: string) { + constructor(zip: JSZipFile, basePath?: string) { this.zip = zip; this.basePath = basePath || ""; } @@ -31,11 +31,11 @@ export default class ZipFileSystem implements FileSystem { return new ZipFileSystem(this.zip, path); } - async create(path: string): Promise { - return new ZipFileWriter(this.zip, path); + async create(path: string, opts?: FileCreateOptions): Promise { + return new ZipFileWriter(this.zip, path, opts); } - async createDir(): Promise { + async createDir(_path: string, _opts?: FileCreateOptions): Promise { // do nothing } @@ -45,15 +45,17 @@ export default class ZipFileSystem implements FileSystem { async list(): Promise { const files: File[] = []; - for (const [filename, details] of Object.entries(this.zip.files)) { - const time = details.date.getTime(); + for (const [filename, jsZipObject] of Object.entries(this.zip.files)) { + const date = jsZipObject.date; // the last modification date + const dateWithOffset = new Date(date.getTime() + date.getTimezoneOffset() * 60000); + const lastModificationDate = dateWithOffset.getTime(); files.push({ name: filename, path: filename, size: 0, digest: "", - createtime: time, - updatetime: time, + createtime: lastModificationDate, + updatetime: lastModificationDate, }); } return files; diff --git a/scripts/pack.js b/scripts/pack.js index a72bb081c..925120e15 100644 --- a/scripts/pack.js +++ b/scripts/pack.js @@ -1,7 +1,7 @@ /* global process */ import { promises as fs } from "fs"; import { createWriteStream } from "fs"; -import JSZip from "jszip"; +import { createJSZip } from "@App/pkg/utils/jszip-x"; import ChromeExtension from "crx"; import { execSync } from "child_process"; import manifest from "../src/manifest.json" with { type: "json" }; @@ -90,8 +90,8 @@ firefoxManifest.commands = { _execute_action: {}, }; -const chrome = new JSZip(); -const firefox = new JSZip(); +const chrome = createJSZip(); +const firefox = createJSZip(); async function addDir(zip, localDir, toDir, filters) { const sub = async (localDir, toDir) => { diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index 62429a850..520ae3bcb 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -50,8 +50,15 @@ export class ScriptClient extends Client { return this.do<[boolean, ScriptInfo]>("getInstallInfo", uuid); } - install(script: Script, code: string, upsertBy: InstallSource = "user"): Promise<{ update: boolean }> { - return this.doThrow("install", { script, code, upsertBy }); + install(params: { + details: Script; + code: string; + upsertBy?: InstallSource; + createtime?: number; + updatetime?: number; + }): Promise<{ update: boolean }> { + if (!params.upsertBy) params.upsertBy = "user"; + return this.doThrow("install", { ...params }); } // delete(uuid: string) { diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index fd669b14e..ed6d5140f 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -234,7 +234,7 @@ export class ScriptService { const { script } = await prepareScriptByCode(code, url, uuid); script.subscribeUrl = subscribeUrl; await this.installScript({ - script, + details: script, code, upsertBy: source, }); @@ -246,7 +246,7 @@ export class ScriptService { const { code, upsertBy, uuid } = param; const { script } = await prepareScriptByCode(code, "", uuid, true); await this.installScript({ - script, + details: script, code, upsertBy, }); @@ -266,9 +266,15 @@ export class ScriptService { } // 安装脚本 / 更新腳本 - async installScript(param: { script: Script; code: string; upsertBy: InstallSource }) { + async installScript(param: { + details: Script; + code: string; + upsertBy?: InstallSource; + createtime?: number; + updatetime?: number; + }) { param.upsertBy = param.upsertBy || "user"; - const { script, upsertBy } = param; + const { details: script, upsertBy, createtime, updatetime } = param; // 删 storage cache const compiledResourceUpdatePromise = this.compiledResourceDAO.delete(script.uuid); const logger = this.logger.with({ @@ -286,6 +292,14 @@ export class ScriptService { script.selfMetadata = oldScript.selfMetadata; } if (script.ignoreVersion) script.ignoreVersion = ""; + if (createtime) { + script.createtime = createtime; + } + if (updatetime) { + script.updatetime = updatetime; + } + console.log(12388, createtime, updatetime); + console.log(new Error().stack); return this.scriptDAO .save(script) .then(async () => { @@ -720,7 +734,7 @@ export class ScriptService { if (checkSilenceUpdate(oldScript!.metadata, script.metadata)) { logger?.info("silence update script"); await this.installScript({ - script, + details: script, code, upsertBy, }); @@ -903,7 +917,7 @@ export class ScriptService { const { script } = await prepareScriptByCode(code, url, uuid); console.log("slienceUpdate", script.name); await this.installScript({ - script, + details: script, code, upsertBy: "system", }); diff --git a/src/app/service/service_worker/synchronize.ts b/src/app/service/service_worker/synchronize.ts index da3a295a8..7779ab497 100644 --- a/src/app/service/service_worker/synchronize.ts +++ b/src/app/service/service_worker/synchronize.ts @@ -12,7 +12,7 @@ import { isWarpTokenError } from "@Packages/filesystem/error"; import type { Group } from "@Packages/message/server"; import type { MessageSend } from "@Packages/message/types"; import { type IMessageQueue } from "@Packages/message/message_queue"; -import JSZip from "jszip"; +import { createJSZip } from "@App/pkg/utils/jszip-x"; import { type ValueService } from "./value"; import { type ResourceService } from "./resource"; import { createObjectURL } from "../offscreen/client"; @@ -114,6 +114,10 @@ export class SynchronizeService { if (!code) { throw new Error(`Script ${script.uuid} code not found`); } + const storage: ValueStorage = { + data: {}, + ts: Date.now(), + }; const ret = { code: code.code, options: { @@ -126,20 +130,18 @@ export class SynchronizeService { name: script.name, uuid: script.uuid, sc_uuid: script.uuid, - modified: script.updatetime, - file_url: script.downloadUrl, + modified: script.updatetime!, + file_url: script.downloadUrl!, subscribe_url: script.subscribeUrl, }, }, // storage, - requires: [], - requiresCss: [], - resources: [], - } as unknown as ScriptBackupData; - const storage: ValueStorage = { - data: {}, - ts: Date.now(), - }; + requires: [] as ResourceBackup[], + requiresCss: [] as ResourceBackup[], + resources: [] as ResourceBackup[], + storage, + lastModificationDate: script.updatetime || script.createtime || undefined, + } satisfies ScriptBackupData; const values = await this.value.getScriptValue(script); for (const key of Object.keys(values)) { storage.data[key] = values[key]; @@ -240,7 +242,7 @@ export class SynchronizeService { // 请求导出文件 async requestExport(uuids?: string[]) { - const zip = new JSZip(); + const zip = createJSZip(); const fs = new ZipFileSystem(zip); await this.backup(fs, uuids); // 生成文件,并下载 @@ -264,7 +266,7 @@ export class SynchronizeService { // 备份到云端 async backupToCloud({ type, params }: { type: FileSystemType; params: any }) { // 首先生成zip文件 - const zip = new JSZip(); + const zip = createJSZip(); const fs = new ZipFileSystem(zip); await this.backup(fs); this.logger.info("backup to cloud"); @@ -589,7 +591,7 @@ export class SynchronizeService { ); script.origin = script.origin || metaObj.origin; this.script.installScript({ - script, + details: script, code, upsertBy: "sync", }); diff --git a/src/pages/components/CloudScriptPlan/index.tsx b/src/pages/components/CloudScriptPlan/index.tsx index 698f28877..09d2b73d9 100644 --- a/src/pages/components/CloudScriptPlan/index.tsx +++ b/src/pages/components/CloudScriptPlan/index.tsx @@ -9,7 +9,7 @@ import { IconQuestionCircleFill } from "@arco-design/web-react/icon"; import type { ExportParams } from "@Packages/cloudscript/cloudscript"; import { parseExportCookie, parseExportValue } from "@Packages/cloudscript/cloudscript"; import CloudScriptFactory from "@Packages/cloudscript/factory"; -import JSZip from "jszip"; +import { createJSZip } from "@App/pkg/utils/jszip-x"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; @@ -117,7 +117,7 @@ const CloudScriptPlan: React.FC<{ const values = await parseExportValue(script, params.exportValue); const cookies = await parseExportCookie(params.exportCookie); if (cloudScriptType === "local") { - const jszip = new JSZip(); + const jszip = createJSZip(); const cloudScript = CloudScriptFactory.create("local", { zip: jszip, ...params, diff --git a/src/pages/import/App.tsx b/src/pages/import/App.tsx index e74b3e4b2..7188dc96c 100644 --- a/src/pages/import/App.tsx +++ b/src/pages/import/App.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { Button, Card, Checkbox, Divider, List, Message, Space, Switch, Typography } from "@arco-design/web-react"; import { useTranslation } from "react-i18next"; // 导入react-i18next的useTranslation钩子 -import JSZip from "jszip"; +import { loadAsyncJSZip } from "@App/pkg/utils/jszip-x"; import type { ScriptOptions, ScriptData, SubscribeData } from "@App/pkg/backup/struct"; import { prepareScriptByCode } from "@App/pkg/utils/script"; import { SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE, ScriptDAO } from "@App/app/repo/scripts"; @@ -91,7 +91,7 @@ function App() { const resp = await cacheInstance.get<{ filename: string; url: string }>(cacheKey); if (!resp) throw new Error("fetchData failed"); const filedata = await fetch(resp.url).then((resp) => resp.blob()); - const zip = await JSZip.loadAsync(filedata); + const zip = await loadAsyncJSZip(filedata); const backData = await parseBackupZipFile(zip); const backDataScript = backData.script as ScriptData[]; @@ -167,7 +167,10 @@ function App() { if (item.script?.script) { if (item.script.script.ignoreVersion) item.script.script.ignoreVersion = ""; } - await scriptClient.install(item.script!.script!, item.code); + const scriptDetails = item.script!.script!; + const createtime = item.lastModificationDate; + const updatetime = item.lastModificationDate; + await scriptClient.install({ details: scriptDetails, code: item.code, createtime, updatetime }); await Promise.all([ (async () => { // 导入资源 diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index cf12356d2..82b75f0e2 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -61,7 +61,7 @@ function App() { const installOrUpdateScript = async (newScript: Script, code: string) => { if (newScript.ignoreVersion) newScript.ignoreVersion = ""; - await scriptClient.install(newScript, code); + await scriptClient.install({ details: newScript, code }); const metadata = newScript.metadata; setScriptInfo((prev) => (prev ? { ...prev, code, metadata } : prev)); setOldScriptVersion(metadata!.version![0]); @@ -312,7 +312,7 @@ function App() { (upsertScript as Script).checkUpdate = false; } // 故意只安装或执行,不改变显示内容 - await scriptClient.install(upsertScript as Script, scriptCode); + await scriptClient.install({ details: upsertScript as Script, code: scriptCode }); if (isUpdate) { Message.success(t("install.update_success")!); setBtnText(t("install.update_success")!); @@ -324,7 +324,7 @@ function App() { } if ((upsertScript as Script).ignoreVersion) (upsertScript as Script).ignoreVersion = ""; // 故意只安装或执行,不改变显示内容 - await scriptClient.install(upsertScript as Script, scriptCode); + await scriptClient.install({ details: upsertScript as Script, code: scriptCode }); if (isUpdate) { Message.success(t("install.update_success")!); setBtnText(t("install.update_success")!); diff --git a/src/pages/options/routes/script/ScriptEditor.tsx b/src/pages/options/routes/script/ScriptEditor.tsx index 4e095c201..f8aa841ec 100644 --- a/src/pages/options/routes/script/ScriptEditor.tsx +++ b/src/pages/options/routes/script/ScriptEditor.tsx @@ -246,7 +246,7 @@ function ScriptEditor() { } if (script.ignoreVersion) script.ignoreVersion = ""; return scriptClient - .install(script, code) + .install({ details: script, code }) .then((update): Script => { if (!update) { Message.success(t("create_success_note")); diff --git a/src/pkg/backup/backup.test.ts b/src/pkg/backup/backup.test.ts index 596e82cd3..a5aad1026 100644 --- a/src/pkg/backup/backup.test.ts +++ b/src/pkg/backup/backup.test.ts @@ -1,4 +1,4 @@ -import JSZip from "jszip"; +import { createJSZip } from "@App/pkg/utils/jszip-x"; import BackupExport from "./export"; import { parseBackupZipFile } from "./utils"; import type { BackupData } from "./struct"; @@ -7,7 +7,7 @@ import ZipFileSystem from "@Packages/filesystem/zip/zip"; describe.concurrent("backup", () => { it.concurrent("empty", async () => { - const zipFile = new JSZip(); + const zipFile = createJSZip(); const fs = new ZipFileSystem(zipFile); await new BackupExport(fs).export({ script: [], @@ -21,7 +21,7 @@ describe.concurrent("backup", () => { }); it.concurrent("export and import script - basic", async () => { - const zipFile = new JSZip(); + const zipFile = createJSZip(); const fs = new ZipFileSystem(zipFile); const data: BackupData = { script: [ @@ -112,7 +112,7 @@ describe.concurrent("backup", () => { }); it.concurrent("export and import script - name and version only", async () => { - const zipFile = new JSZip(); + const zipFile = createJSZip(); const fs = new ZipFileSystem(zipFile); const data: BackupData = { script: [ @@ -180,7 +180,7 @@ describe.concurrent("backup", () => { }); it.concurrent("export and import script - 2 scripts", async () => { - const zipFile = new JSZip(); + const zipFile = createJSZip(); const fs = new ZipFileSystem(zipFile); const data: BackupData = { script: [ @@ -293,7 +293,7 @@ describe.concurrent("backup", () => { }); it.concurrent("export and import script - 30 scripts + 20 subscribes", async () => { - const zipFile = new JSZip(); + const zipFile = createJSZip(); const fs = new ZipFileSystem(zipFile); const data: BackupData = { script: Array.from({ length: 30 }, (v, i) => { diff --git a/src/pkg/backup/export.ts b/src/pkg/backup/export.ts index 57a30b439..6797648db 100644 --- a/src/pkg/backup/export.ts +++ b/src/pkg/backup/export.ts @@ -3,6 +3,7 @@ import { base64ToBlob } from "../utils/utils"; import { toStorageValueStr } from "../utils/utils"; import type { BackupData, ResourceBackup, ScriptBackupData, SubscribeBackupData } from "./struct"; import { md5OfText } from "../utils/crypto"; +import type { FileCreateOptions } from "@Packages/filesystem/filesystem"; export default class BackupExport { fs: FileSystem; @@ -42,24 +43,26 @@ export default class BackupExport { } const wrtieStorage = JSON.stringify(storage); + const fileOpts = { modifiedDate: script.lastModificationDate } as FileCreateOptions; return [ // 写脚本文件 - this.fs.create(`${filename}.user.js`).then((fileWriter) => fileWriter.write(writeCode)), + this.fs.create(`${filename}.user.js`, fileOpts).then((fileWriter) => fileWriter.write(writeCode)), // 写入脚本options.json - this.fs.create(`${filename}.options.json`).then((fileWriter) => fileWriter.write(writeOptions)), + this.fs.create(`${filename}.options.json`, fileOpts).then((fileWriter) => fileWriter.write(writeOptions)), // 写入脚本storage.json - this.fs.create(`${filename}.storage.json`).then((fileWriter) => fileWriter.write(wrtieStorage)), + this.fs.create(`${filename}.storage.json`, fileOpts).then((fileWriter) => fileWriter.write(wrtieStorage)), // 写入脚本资源文件 - ...this.writeResource(filename, script.resources, "resources"), - ...this.writeResource(filename, script.requires, "requires"), - ...this.writeResource(filename, script.requiresCss, "requires.css"), + ...this.writeResource(filename, script.resources, "resources", fileOpts), + ...this.writeResource(filename, script.requires, "requires", fileOpts), + ...this.writeResource(filename, script.requiresCss, "requires.css", fileOpts), ]; } writeResource( filename: string, resources: ResourceBackup[], - type: "resources" | "requires" | "requires.css" + type: "resources" | "requires" | "requires.css", + fileOpts: FileCreateOptions ): Promise[] { return resources.flatMap((item) => { // md5是tm的导出规则 @@ -68,10 +71,10 @@ export default class BackupExport { const writeMeta = JSON.stringify(item.meta); return [ this.fs - .create(`${filename}.user.js-${md5}-${item.meta.name}`) + .create(`${filename}.user.js-${md5}-${item.meta.name}`, fileOpts) .then((fileWriter) => fileWriter.write(writeSource)), this.fs - .create(`${filename}.user.js-${md5}-${item.meta.name}.${type}.json`) + .create(`${filename}.user.js-${md5}-${item.meta.name}.${type}.json`, fileOpts) .then((fileWriter) => fileWriter.write(writeMeta)), ]; }); diff --git a/src/pkg/backup/import.ts b/src/pkg/backup/import.ts index 347bba60f..690698848 100644 --- a/src/pkg/backup/import.ts +++ b/src/pkg/backup/import.ts @@ -49,8 +49,8 @@ export default class BackupImport { // 解析出备份数据 async parse(): Promise { - const map = new Map(); - const subscribe = new Map(); + const map = new Map & ScriptBackupData>(); + const subscribe = new Map & SubscribeBackupData>(); let files = await this.fs.list(); // 处理订阅 @@ -62,7 +62,8 @@ export default class BackupImport { const key = name.substring(0, name.length - 12); const subData = { source: await this.getFileContent(file, false), - } as SubscribeBackupData; + lastModificationDate: file.updatetime, + } satisfies Partial & SubscribeBackupData; subscribe.set(key, subData); return true; }); @@ -92,7 +93,8 @@ export default class BackupImport { requires: [], requiresCss: [], resources: [], - } as ScriptBackupData; + lastModificationDate: file.updatetime, + } satisfies Partial & ScriptBackupData; map.set(key, backupData); return true; }); @@ -229,8 +231,8 @@ export default class BackupImport { // 将map转化为数组 return { - script: Array.from(map.values()), - subscribe: Array.from(subscribe.values()), + script: [...map.values()] as ScriptData[], + subscribe: [...subscribe.values()] as SubscribeData[], }; } diff --git a/src/pkg/backup/struct.ts b/src/pkg/backup/struct.ts index 08488d5cf..e05b8f50f 100644 --- a/src/pkg/backup/struct.ts +++ b/src/pkg/backup/struct.ts @@ -74,6 +74,7 @@ export type ScriptBackupData = { resources: ResourceBackup[]; // 为了兼容暴力猴而设置的字段 enabled?: boolean; + lastModificationDate?: number; }; export type ScriptData = ScriptBackupData & { @@ -107,6 +108,7 @@ export type SubscribeOptionsFile = { export type SubscribeBackupData = { source: string; options?: SubscribeOptionsFile; + lastModificationDate?: number; }; export type BackupData = { diff --git a/src/pkg/backup/utils.ts b/src/pkg/backup/utils.ts index dc6ef0696..65db5290d 100644 --- a/src/pkg/backup/utils.ts +++ b/src/pkg/backup/utils.ts @@ -1,9 +1,9 @@ import ZipFileSystem from "@Packages/filesystem/zip/zip"; -import type JSZip from "jszip"; +import { type JSZipFile } from "@App/pkg/utils/jszip-x"; import BackupImport from "./import"; // 解析备份文件 -export function parseBackupZipFile(zip: JSZip) { +export function parseBackupZipFile(zip: JSZipFile) { const fs = new ZipFileSystem(zip); // 解析文件 return new BackupImport(fs).parse(); diff --git a/src/pkg/utils/jszip-x.ts b/src/pkg/utils/jszip-x.ts new file mode 100644 index 000000000..bea81a717 --- /dev/null +++ b/src/pkg/utils/jszip-x.ts @@ -0,0 +1,77 @@ +/** + * + * JSZIP 由于不再更新,问题只能手改。 + * + * UTC时间问题 + * https://github.com/Stuk/jszip/issues/369#issuecomment-546204220 + * https://blog.csdn.net/weixin_45410246/article/details/150015478 + * + * Typescript: Fix missing types for JSZip.defaults + * https://github.com/Stuk/jszip/pull/927 + * https://github.com/Stuk/jszip/issues/690 + * + * + * 日后应考虑 fork 一下加入以下PR + * + * 修正单一档案不能大于 2GB + * https://github.com/Stuk/jszip/pull/791 + * + * + * 其他参考: + * https://greasyfork.org/scripts/526002-gitzip-lite/code + * + */ +import JSZip from "jszip"; + +type Compression = "STORE" | "DEFLATE"; + +interface CompressionOptions { + level: number; +} + +interface InputByType { + base64: string; + string: string; + text: string; + binarystring: string; + array: number[]; + uint8array: Uint8Array; + arraybuffer: ArrayBuffer; + blob: Blob; + stream: NodeJS.ReadableStream; +} + +type InputFileFormat = InputByType[keyof InputByType] | Promise; + +interface JSZipDefaults { + base64: boolean; // default false + binary: boolean; // default false + dir: boolean; // default false + createFolders: boolean; // default true + date: Date; // default null + compression: Compression | null; // default null + compressionOptions: CompressionOptions | null; // default null + comment: string | null; // default null + unixPermissions: number | string | null; // default null + dosPermissions: number | null; // default null +} + +type JSZipWithDefaults = typeof JSZip & { defaults: JSZipDefaults }; + +const JSZipX = JSZip as JSZipWithDefaults; + +export const createJSZip = () => { + const currDate = new Date(); + const dateWithOffset = new Date(currDate.getTime() - currDate.getTimezoneOffset() * 60000); + // replace the default date with dateWithOffset + JSZipX.defaults.date = dateWithOffset; + return new JSZipX(); +}; + +export const loadAsyncJSZip = (content: InputFileFormat, options?: JSZip.JSZipLoadOptions): Promise => { + return createJSZip().loadAsync(content, options) as Promise; +}; + +export type JSZipFile = typeof JSZipX; + +export type JSZipFileOptions = JSZip.JSZipFileOptions;