diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e4ced6e..51ea155 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: ["https://paypal.me/Molunerfinn"] \ No newline at end of file +custom: ["https://paypal.me/Kuingsmile"] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c79d42e..61bc251 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -3,12 +3,12 @@ description: 提交一个问题 / Report a bug title: "[Bug]: " labels: ["bug"] assignees: - - molunerfinn + - Kuingsmile body: - type: markdown attributes: value: |+ - ## PicGo Issue 模板 + ## PicList Issue 模板 请依照该模板来提交,否则将会被关闭。 **提问之前请注意你看过 FAQ、文档以及那些被关闭的 issues。否则同样的提问也会被关闭!** @@ -24,15 +24,15 @@ body: options: - label: "[文档/Doc](https://picgo.github.io/PicGo-Doc/)" required: true - - label: "[Issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)" + - label: "[Issues](https://github.com/Kuingsmile/PicList/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)" required: true - - label: "[FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md)" + - label: "[FAQ](https://github.com/Kuingsmile/PicList/blob/dev/FAQ.md)" required: true - type: input id: version attributes: - label: PicGo的版本 | PicGo Version - placeholder: 例如 v2.3.0-beta.1 + label: PicList的版本 | PicList Version + placeholder: 例如 v0.0.1 validations: required: true - type: dropdown @@ -58,11 +58,11 @@ body: id: log attributes: label: 相关日志 | Logs - description: 请附上 PicGo 的相关报错日志(用文本的形式)。报错日志可以在 PicGo 设置 -> 设置日志文件 -> 点击打开 后找到 | Please attach PicGo's relevant error log (in text form). The error log can be found in PicGo Settings -> Set Log File -> Click to Open + description: 请附上 PicList 的相关报错日志(用文本的形式)。报错日志可以在 PicList 设置 -> 设置日志文件 -> 点击打开 后找到 | Please attach PicList's relevant error log (in text form). The error log can be found in PicList Settings -> Set Log File -> Click to Open - type: markdown attributes: value: | - 最后,喜欢 PicGo 的话不妨给它点个 star~ + 最后,喜欢 PicList 的话不妨给它点个 star~ 如果可以的话,请我喝杯咖啡?首页有赞助二维码,谢谢你的支持! - Finally, if you like PicGo, give it a star~ + Finally, if you like PicList, give it a star~ Buy me a cup of coffee if you can? There is a sponsorship QR code on the homepage, thank you for your support! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 4df34da..b91dc7a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -3,12 +3,12 @@ description: 功能请求 / Feature request title: "[Feature]: " labels: ["feature request"] assignees: - - molunerfinn + - Kuingsmile body: - type: markdown attributes: value: |+ - ## PicGo Issue 模板 + ## PicList Issue 模板 请依照该模板来提交,否则将会被关闭。 **提问之前请注意你看过 FAQ、文档以及那些被关闭的 issues。否则同样的提问也会被关闭!** @@ -24,15 +24,15 @@ body: options: - label: "[文档/Doc](https://picgo.github.io/PicGo-Doc/)" required: true - - label: "[Issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)" + - label: "[Issues](https://github.com/Kuingsmile/PicList/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)" required: true - - label: "[FAQ](https://github.com/Molunerfinn/PicGo/blob/dev/FAQ.md)" + - label: "[FAQ](https://github.com/Kuingsmile/PicList/blob/dev/FAQ.md)" required: true - type: input id: version attributes: - label: PicGo的版本 | PicGo Version - placeholder: 例如 v2.3.0-beta.1 + label: PicList的版本 | PicList Version + placeholder: 例如 v0.0.1 validations: required: true - type: dropdown @@ -57,7 +57,7 @@ body: - type: markdown attributes: value: | - 最后,喜欢 PicGo 的话不妨给它点个 star~ + 最后,喜欢 PicList 的话不妨给它点个 star~ 如果可以的话,请我喝杯咖啡?首页有赞助二维码,谢谢你的支持! - Finally, if you like PicGo, give it a star~ + Finally, if you like PicList, give it a star~ Buy me a cup of coffee if you can? There is a sponsorship QR code on the homepage, thank you for your support! \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 17ea7c7..a129b56 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,13 +1,13 @@ # main.yml # Workflow's name -name: Build +name: Auto Build # Workflow's trigger on: push: branches: - - master + - release # Workflow's jobs jobs: @@ -54,5 +54,6 @@ jobs: yarn upload-dist env: GH_TOKEN: ${{ secrets.GH_TOKEN }} - PICGO_ENV_COS_SECRET_ID: ${{ secrets.PICGO_ENV_COS_SECRET_ID }} - PICGO_ENV_COS_SECRET_KEY: ${{ secrets.PICGO_ENV_COS_SECRET_KEY }} + R2_SECRET_ID: ${{ secrets.R2_SECRET_ID }} + R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }} + R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} diff --git a/.github/workflows/manually.yml b/.github/workflows/manually.yml index b149b47..afb88b4 100644 --- a/.github/workflows/manually.yml +++ b/.github/workflows/manually.yml @@ -1,7 +1,7 @@ # main.yml # Workflow's name -name: Build +name: Manually Build # Workflow's trigger on: workflow_dispatch @@ -51,5 +51,6 @@ jobs: yarn upload-dist env: GH_TOKEN: ${{ secrets.GH_TOKEN }} - PICGO_ENV_COS_SECRET_ID: ${{ secrets.PICGO_ENV_COS_SECRET_ID }} - PICGO_ENV_COS_SECRET_KEY: ${{ secrets.PICGO_ENV_COS_SECRET_KEY }} + R2_SECRET_ID: ${{ secrets.R2_SECRET_ID }} + R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }} + R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} diff --git a/.gitignore b/.gitignore index 94a9fc0..c899d13 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ dist_electron/ test.js .env scripts/*.yml +scripts/generateYmlFile.js #Electron-builder output -/dist_electron \ No newline at end of file +/dist_electron +/docs +cloc.exe \ No newline at end of file diff --git a/356u2spwu37 b/356u2spwu37 new file mode 100644 index 0000000..bb448f6 Binary files /dev/null and b/356u2spwu37 differ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0499e8..c09ccfb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -## 贡献指南 +# 贡献指南 -### 安装与启动 +## 安装与启动 1. 使用 [yarn](https://yarnpkg.com/) 安装依赖 @@ -22,16 +22,18 @@ yarn dev 4. 所有的全局类型定义请在 `src/universal/types/` 里添加,如果是 `enum`,请在 `src/universal/types/enum.ts` 里添加。 +5. 与图床管理功能相关的代码请在`src/main/manage`和`src/renderer/manage`目录下添加。 -### i18n -1. 在 `public/i18n/` 下面创建一种语言的 `yml` 文件,例如 `zh-Hans.yml`。然后参考 `zh-CN.yml` 或者 `en.yml` 编写语言文件。并注意,PicGo 会通过语言文件中的 `LANG_DISPLAY_LABEL` 向用户展示该语言的名称。 +## i18n + +1. 在 `public/i18n/` 下面创建一种语言的 `yml` 文件,例如 `zh-Hans.yml`。然后参考 `zh-CN.yml` 或者 `en.yml` 编写语言文件。并注意,PicList 会通过语言文件中的 `LANG_DISPLAY_LABEL` 向用户展示该语言的名称。 2. 在 `src/universal/i18n/index.ts` 里添加一种默认语言。其中 `label` 就是语言文件中 `LANG_DISPLAY_LABEL` 的值,`value` 是语言文件名。 3. 如果是对已有语言文件进行更新,请在更新完,务必运行一遍 `yarn gen-i18n`,确保能生成正确的语言定义文件。 -### 提交代码 +## 提交代码 1. 请检查代码没有多余的注释、`console.log` 等调试代码。 2. 提交代码前,请执行命令 `git add . && yarn cz`,唤起 PicGo 的[代码提交规范工具](https://github.com/PicGo/bump-version)。通过该工具提交代码。 diff --git a/CONTRIBUTING_EN.md b/CONTRIBUTING_EN.md index 1f962f6..df17415 100644 --- a/CONTRIBUTING_EN.md +++ b/CONTRIBUTING_EN.md @@ -1,6 +1,6 @@ -## Contribution Guidelines +# Contribution Guidelines -### Installation and startup +## Installation and startup 1. Use [yarn](https://yarnpkg.com/) to install dependencies @@ -22,16 +22,17 @@ Startup project. 4. Please add all global type definitions in `src/universal/types/`, if it is `enum`, please add it in `src/universal/types/enum.ts`. +5. Code related to the management function of the picture bed should be added in the `src/main/manage` and `src/renderer/manage` directory. -### i18n +## i18n -1. Create a language `yml` file under `public/i18n/`, for example `zh-Hans.yml`. Then refer to `zh-CN.yml` or `en.yml` to write language files. Also note that PicGo will display the name of the language to the user via `LANG_DISPLAY_LABEL` in the language file. +1. Create a language `yml` file under `public/i18n/`, for example `zh-Hans.yml`. Then refer to `zh-CN.yml` or `en.yml` to write language files. Also note that PicList will display the name of the language to the user via `LANG_DISPLAY_LABEL` in the language file. 2. Add a default language to `src/universal/i18n/index.ts`. where `label` is the value of `LANG_DISPLAY_LABEL` in the language file, and `value` is the name of the language file. 3. If you are updating an existing language file, be sure to run `yarn gen-i18n` after the update to ensure that the correct language definition file can be generated. -### Submit code +## Submit code 1. Please check that the code has no extra comments, `console.log` and other debugging code. -2. Before submitting the code, please execute the command `git add . && yarn cz` to invoke PicGo's [Code Submission Specification Tool](https://github.com/PicGo/bump-version). Submit code through this tool. \ No newline at end of file +2. Before submitting the code, please execute the command `git add . && yarn cz` to invoke PicGo's [Code Submission Specification Tool](https://github.com/PicGo/bump-version). Submit code through this tool. diff --git a/FAQ.md b/FAQ.md index 9c3b1a8..603ad72 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1,80 +1,85 @@ +# FAQ + +该FAQ修改自PicGo的FAQ,感谢PicGo的作者Molunerfinn。 + ## 常见问题 -> 在使用 PicGo 期间你会遇到很多问题,不过很多问题其实之前就有人提问过,也被解决,所以你可以先看看 [使用文档](https://picgo.github.io/PicGo-Doc/zh/guide/getting-started.html#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B),这份 FAQ,以及那些被关闭的 [issues](https://github.com/Molunerfinn/PicGo/issues?q=is%3Aissue+is%3Aclosed),应该能找到答案。 +> 本软件的上传工具部分来自PicGo,基本没有改动,请参考PicGo的 [使用文档](https://picgo.github.io/PicGo-Doc/zh/guide/getting-started.html#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B) -## 1. 七牛图床上传图片成功后,相册里无法显示或图片无`http://`前缀 +## 1. PicList和PicGo有什么关系? -通常是你的七牛图床配置里的`设定访问网址`没有加上`http://`或者`https//`头。 +PicList项目fork自PicGo项目,基于PicGo进行了二次开发,添加了如下功能: -参考:[issue#79](https://github.com/Molunerfinn/PicGo/issues/79) +注意:以下功能已适配的图床包括:阿里云 OSS、腾讯云 COS、七牛云 Kodo、又拍云、SM.MS、Imgur、GitHub。 -## 2. 能否支持图床远端同步删除 +- 相册中可同步删除云端图片 +- 支持管理所有图床,可以在线进行云端目录查看、文件搜索、批量上传、批量下载、删除文件和图片预览等 +- 对于私有存储桶等支持复制预签名链接进行分享 +- 优化了PicGo的界面,解锁了窗口大小限制,同时美化了部分界面布局 -不能。有些图床(比如微博图床、SM.MS、Imgur 等)不支持后台管理,为了架构统一不支持远端删除。 +PicList所有新功能的添加没有影响到PicGo的原有功能,所以你可以在PicList中使用PicGo的所有插件。同时仍然可以配合typora、obsidian等软件进行使用。 -## 3. 能否支持上传视频文件 +## 2. 使用图床管理功能时,出现无法获取目录等错误 -目前不能。如果有人开发了相应的插件理论可以支持任意文件上传。 +请查看日志文件`manage.log`,此外,各平台的API调用基本都有每小时次数限制,如果出现错误,请稍后再试。 -## 4. 微博图床上传之后无法显示预览图 +## 3. 支持哪些图床远端同步删除 -通常是挂了全局代理导致的。 +可以,本软件基于PicGo进行了二次开发,添加了远端同步删除功能。但是需要你的图床支持,目前支持的图床有: -参考:[issue36](https://github.com/Molunerfinn/PicGo/issues/36) +- 阿里云 OSS +- 腾讯云 COS +- 七牛云 Kodo +- 又拍云 +- SM.MS +- Imgur +- GitHub + +## 4. 能否支持上传视频文件 + +可以,通过新添加的图床管理功能,你可以上传任意格式的文件,包括视频文件。同时,在管理界面内上传时,使用分片上传/流式上传等方式,相对于PicGo内置的转换为base64的方式,上传更快,更稳定。 ## 5. 能否支持某某某图床 -截止 v1.6,PicGo 支持了如下图床: +PicGo本体支持了如下图床: -- `微博图床` v1.0 -- `七牛图床` v1.0 -- `腾讯云 COS v4\v5 版本` v1.1 & v1.5.0 -- `又拍云` v1.2.0 -- `GitHub` v1.5.0 -- `SM.MS` v1.5.1 -- `阿里云 OSS` v1.6.0 -- `Imgur` v1.6.0 +- `七牛图床` +- `腾讯云 COS` +- `又拍云` +- `GitHub` +- `SM.MS` +- `阿里云 OSS` +- `Imgur` -所以本体内将不会再支持其他图床。需要其他图床支持可以参考目前已有的三方 [插件](https://github.com/PicGo/Awesome-PicGo),如果还是没有你所需要的图床欢迎开发一个插件供大家使用。 +PicList在上述7个图床之外,计划整合和优化现有插件,内置更多的常用图床。 -## 6. 一个图床设置多个信息 +此外,PicList兼容PicGo的插件系统,需要其他图床支持可以参考目前已有的PicGo三方 [插件](https://github.com/PicGo/Awesome-PicGo),如果还是没有你所需要的图床欢迎开发一个插件供大家使用。 -不能。因为目前的架构只支持一个图床一份信息。 - -## 7. GitHub 图床有时能上传,有时上传失败 +## 6. Github 图床有时能上传,有时上传失败 1. GitHub 图床不支持上传同名文件,如果有同名文件上传,会报错。建议开启 `时间戳重命名` 避免同名文件。 2. GitHub 服务器和国内 GFW 的问题会导致有时上传成功,有时上传失败,无解。想要稳定请使用付费云存储,如阿里云、腾讯云等,价格也不会贵。 -## 8. Mac 上无法打开 PicGo 的主窗口界面 +## 7. Mac 上无法打开 PicList 的主窗口界面 -PicGo 在 Mac 上是一个顶部栏应用,在 dock 栏是不会有图标的。要打开主窗口,请右键或者双指点按顶部栏 PicGo 图标,选择「打开详细窗口」即可打开主窗口。 +PicList 在 Mac 上是一个顶部栏应用,在 dock 栏是不会有图标的。要打开主窗口,请右键或者双指点按顶部栏 PicList 图标,选择「打开详细窗口」即可打开主窗口。 -## 9. 上传失败,或者是服务器出错 +## 8. 上传失败,或者是服务器出错 -1. PicGo 自带的图床都经过测试,上传出错一般都不是 PicGo 自身的原因。如果你用的是 GitHub 图床请参考上面的第 7 点。 -2. 检查 PicGo 的日志(报错日志可以在 PicGo 设置 -> 设置日志文件 -> 点击打开 后找到),看看 `[PicGo Error]` 的报错信息里有什么关键信息 +1. PicList 自带的图床都经过测试,上传出错一般都不是 PicList 自身的原因。如果你用的是 GitHub 图床请参考上面的第 7 点。 +2. 检查 PicList 的日志(报错日志可以在 PicList 设置 -> 设置日志文件 -> 点击打开 后找到),看看 `[PicList Error]` 的报错信息里有什么关键信息 1. 先自行搜索 error 里的报错信息,往往你能百度或者谷歌出问题原因,不必开 issue。 2. 如果有带有 `401` 、`403` 等 `40X` 状态码字样的,不用怀疑,就是你配置写错了,仔细检查配置,看看是否多了空格之类的。 3. 如果带有 `HttpError`、`RequestError` 、 `socket hang up` 等字样的说明这是网络问题,我无法帮你解决网络问题,请检查你自己的网络,是否有代理,DNS 设置是否正常等。 -3. 通常网络问题引起的上传失败都是因为代理设置不当导致的。如果开启了系统代理,建议同时也在 PicGo 的代理设置中设置对应的HTTP代理。参考 [#912](https://github.com/Molunerfinn/PicGo/issues/912) +3. 通常网络问题引起的上传失败都是因为代理设置不当导致的。如果开启了系统代理,建议同时也在 PicList 的代理设置中设置对应的HTTP代理。 ## 10. macOS版本安装完之后没有主界面 -请找到PicGo在顶部栏的图标,然后右键(触摸板双指点按,或者鼠标右键),即可找到「打开详细窗口」的菜单。 +请找到PicList在顶部栏的图标,然后右键(触摸板双指点按,或者鼠标右键),即可找到「打开详细窗口」的菜单。 -## 11. 相册突然无法显示图片 或者 上传后相册不更新 或者 使用Typora+PicGo上传图片成功但是没有写回Typora +## 11. macOS系统安装完PicList显示「文件已损坏」或者安装完打开没有反应 -这个原因可能是相册存储文件损坏导致的。可以找到 PicGo 配置文件所在路径下的 `picgo.db` ,将其删掉(删掉前建议备份一遍),再重启 PicGo 试试。 -注意同时看看日志文件里有没有什么error,必要时可以提issue。2.3.0以上的版本已经解决因为 `picgo.db` 损坏导致的上述问题,建议更新版本。 - -## 12. Gitee相关问题 - -如果在使用 Gitee 图床的时候遇到上传的问题,由于 PicGo 并没有官方提供 Gitee 上传服务,无法帮你解决,请去你所使用的 Gitee 插件仓库发相关的issue。 - -## 13. macOS系统安装完PicGo显示「文件已损坏」或者安装完打开没有反应 - -因为 PicGo 没有签名,所以会被 macOS 的安全检查所拦下。 +因为 PicList 没有签名,所以会被 macOS 的安全检查所拦下。 1. 安装后打开遇到「文件已损坏」的情况,请按如下方式操作: @@ -84,10 +89,10 @@ PicGo 在 Mac 上是一个顶部栏应用,在 dock 栏是不会有图标的。 sudo spctl --master-disable ``` -然后放行 PicGo : +然后放行 PicList : ``` -xattr -cr /Applications/PicGo.app +xattr -cr /Applications/PicList.app ``` 然后就能正常打开。 @@ -118,9 +123,5 @@ options: 执行命令 ``` -xattr -c /Applications/PicGo.app/* +xattr -c /Applications/PicList.app/* ``` - -2. 如果安装打开后没有反应,请按下方顺序排查: - 1. macOS安装好之后,PicGo 是不会弹出主窗口的,因为 PicGo 在 macOS 系统里设计是个顶部栏应用。注意看你顶部栏的图标,如果有 PicGo 的图标,说明安装成功了,点击图标即可打开顶部栏窗口。参考上述[第八点](#8-mac-上无法打开-picgo-的主窗口界面)。 - 2. 如果你是 M1 的系统,此前装过 PicGo 的 x64 版本,但是后来更新了 arm64 的版本发现打开后没反应,请重启电脑即可。 diff --git a/LICENSE b/LICENSE index d33149c..1cee4bc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,7 @@ The MIT License (MIT) +Copyright (c) 2017-present, Molunerfinn +Copyright (c) 2019 诗人的咸鱼 Copyright (c) 2023-present, KuingSmile Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index 0871354..3f20fd9 100644 --- a/README.md +++ b/README.md @@ -2,43 +2,61 @@

PicList

- + - + - +
-  一款综合了PicGo和AList的图片上传和图床管理桌面工具,基于PicGo,处于早期开发中 +  一款fork自PicGo的二次开发项目,保留了PicGo的所有功能的同时,为相册添加了同步云端删除功能,同时增加了完整的云存储管理功能,包括云端目录查看、文件搜索、批量上传下载和删除文件,复制多种格式文件和图片预览等。 -## 开发计划 +## 特色功能 -本项目的开发初衷是为了解决在个人在使用PicGo桌面版时候的几个痛点: +- 保留了PicGo的所有功能,兼容已有的PicGo插件系统,包括和typora、obsidian等的搭配 +- 相册中可同步删除云端图片 +- 支持管理所有图床,可以在线进行云端目录查看、文件搜索、批量上传、批量下载、删除文件和图片预览等 +- 管理界面使用内置数据库缓存目录,加速目录加载速度 +- 对于私有存储桶等支持复制预签名链接进行分享 +- 优化了PicGo的界面,解锁了窗口大小限制,同时美化了部分界面布局 -1. 相册中无法同步删除云端图片,不小心上传错或者想更换图片时不方便; -2. 只能上传图片,无法上传视频或其它格式文件,在需要向文章中插入其它资源的时候需要自己去上传; -3. 不能查看和复制使用PicGo软件之前上传的图片的链接; -4. 不能从云端取回文件。 +## 下载安装 -为了优化以上问题,基于PicHoro的开发经验,以及使用AList软件时的一些体验,决定基于PicGo开发一款增强版的软件PicList,期望除了PicGo的核心功能外,增加如下功能: +### Github release -1. 相册可同步删除云端图片,支持加强版的图片预览和元信息查看; -2. 支持所有格式和不大于2G的文件的上传; -3. 支持管理所有图床,可以在线进行云端目录查看、文件搜索、上传、下载、删除和文件预览等; -4. 支持不同图床之间的文件复制和移动等; -5. 兼容已有的PicGo插件系统。 +https://github.com/Kuingsmile/PicList/releases -## 开发进度 +### CloudFlare R2 -开发中,预计在2023年2月底之前发布第一个发行版。 +请参考release页面的说明 + + +## 应用截图 +![image](https://user-images.githubusercontent.com/96409857/219062180-ba6de40b-94bb-45be-a510-c4d231920032.png) +![image](https://user-images.githubusercontent.com/96409857/219063188-d7e0b0e7-6e3c-4deb-8bef-0b2b57d2d7ee.png) +![image](https://user-images.githubusercontent.com/96409857/219063398-9a8607df-a1e2-4121-a652-ebd63b38007b.png) + +## 开发说明 + +1. 你需要有 Node、Git 环境,了解 npm 的相关知识。 +2. git clone https://github.com/Kuingsmile/PicList.git 并进入项目。 +yarn 下载依赖。注意如果你没有 yarn,请去 官网 下载安装后再使用。 用 npm install 将导致未知错误! +3. Mac 需要有 Xcode 环境,Windows 需要有 VS 环境。 +4. 如果需要贡献代码,可以参考[贡献指南](https://github.com/Kuingsmile/PicList/blob/dev/CONTRIBUTING.md)。 + +## 其它相关 + +- [PicGo](https://github.com/Molunerfinn/PicGo) : 原版PicGo项目 ## License +本项目基于MIT协议开源,欢迎大家使用和贡献代码,感谢原作者Molunerfinn的开源精神。 + [MIT](https://opensource.org/licenses/MIT) -Copyright (c) 2023 Kuingsmile - +Copyright (c) 2017-present, Molunerfinn +Copyright (c) 2023-present Kuingsmile diff --git a/build/icons/256x256.png b/build/icons/256x256.png index 2951eed..a2ea42e 100644 Binary files a/build/icons/256x256.png and b/build/icons/256x256.png differ diff --git a/build/icons/icon.icns b/build/icons/icon.icns index 113d932..e2e540a 100644 Binary files a/build/icons/icon.icns and b/build/icons/icon.icns differ diff --git a/build/icons/icon.ico b/build/icons/icon.ico index 3f967e1..9028a35 100644 Binary files a/build/icons/icon.ico and b/build/icons/icon.ico differ diff --git a/build/icons/icon2.icns b/build/icons/icon2.icns new file mode 100644 index 0000000..113d932 Binary files /dev/null and b/build/icons/icon2.icns differ diff --git a/build/installer.nsh b/build/installer.nsh index 1b86bf8..a214e7a 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -1,13 +1,13 @@ !macro customInstall SetRegView 64 - WriteRegStr HKCR "*\shell\PicGo" "" "Upload pictures w&ith PicGo" - WriteRegStr HKCR "*\shell\PicGo" "Icon" "$INSTDIR\PicGo.exe" - WriteRegStr HKCR "*\shell\PicGo\command" "" '"$INSTDIR\PicGo.exe" "upload" "%1"' + WriteRegStr HKCR "*\shell\PicList" "" "Upload pictures w&ith PicList" + WriteRegStr HKCR "*\shell\PicList" "Icon" "$INSTDIR\PicList.exe" + WriteRegStr HKCR "*\shell\PicList\command" "" '"$INSTDIR\PicList.exe" "upload" "%1"' SetRegView 32 - WriteRegStr HKCR "*\shell\PicGo" "" "Upload pictures w&ith PicGo" - WriteRegStr HKCR "*\shell\PicGo" "Icon" "$INSTDIR\PicGo.exe" - WriteRegStr HKCR "*\shell\PicGo\command" "" '"$INSTDIR\PicGo.exe" "upload" "%1"' + WriteRegStr HKCR "*\shell\PicList" "" "Upload pictures w&ith PicList" + WriteRegStr HKCR "*\shell\PicList" "Icon" "$INSTDIR\PicList.exe" + WriteRegStr HKCR "*\shell\PicList\command" "" '"$INSTDIR\PicList.exe" "upload" "%1"' !macroend !macro customUninstall - DeleteRegKey HKCR "*\shell\PicGo" + DeleteRegKey HKCR "*\shell\PicList" !macroend diff --git a/docs/APP.vue b/docs/APP.vue deleted file mode 100644 index a447d9a..0000000 --- a/docs/APP.vue +++ /dev/null @@ -1,216 +0,0 @@ - - - diff --git a/docs/main.js b/docs/main.js deleted file mode 100644 index 903e5e1..0000000 --- a/docs/main.js +++ /dev/null @@ -1,10 +0,0 @@ -import Vue from 'vue' -import App from './APP.vue' -import 'melody.css' -import axios from 'axios' - -Vue.prototype.$http = axios - -new Vue({ - render: h => h(App) -}).$mount('#app') diff --git a/docs/template.html b/docs/template.html deleted file mode 100644 index 88af599..0000000 --- a/docs/template.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - PicGo - - -
- - \ No newline at end of file diff --git a/package.json b/package.json index c9f0256..d3f4603 100644 --- a/package.json +++ b/package.json @@ -15,28 +15,43 @@ "postinstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps", "release": "vue-cli-service electron:build --publish always", - "upload-dist": "node ./scripts/upload-dist-to-cos.js" + "upload-dist": "node ./scripts/upload-dist-to-r2.js" }, "dependencies": { "@element-plus/icons-vue": "^2.0.10", + "@imengyu/vue3-context-menu": "^1.2.2", + "@octokit/rest": "^19.0.7", "@picgo/i18n": "^1.0.0", "@picgo/store": "^2.0.4", - "axios": "^0.19.0", + "@types/mime-types": "^2.1.1", + "ali-oss": "^6.17.1", + "aws-sdk": "^2.1304.0", + "axios": "^1.3.2", "compare-versions": "^4.1.3", "core-js": "^3.27.1", + "cos-nodejs-sdk-v5": "^2.11.19", "custom-electron-titlebar": "^4.1.5", - "element-plus": "^2.2.28", - "fs-extra": "^10.0.0", - "js-yaml": "^4.1.0", + "dexie": "^3.2.3", + "element-plus": "^2.2.30", + "fast-xml-parser": "^4.1.1", + "form-data": "^4.0.0", + "fs-extra": "^11.1.0", + "got": "^12.5.3", + "hpagent": "^1.2.0", "keycode": "^2.2.0", "lodash-id": "^0.14.0", "lowdb": "^1.0.0", + "mime-types": "^2.1.35", "mitt": "^3.0.0", - "picgo": "^1.5.0", + "piclist": "^0.0.8", + "pinia": "^2.0.29", + "pinia-plugin-persistedstate": "^3.0.2", + "qiniu": "^7.8.0", "qrcode.vue": "^3.3.3", - "shell-path": "2.1.0", + "shell-path": "3.0.0", + "upyun": "^3.4.6", "uuid": "^9.0.0", - "vue": "^3.2.45", + "vue": "^3.2.47", "vue-router": "^4.1.6", "vue3-lazyload": "^0.3.6", "vue3-photo-preview": "^0.2.9", @@ -45,8 +60,9 @@ "devDependencies": { "@babel/plugin-proposal-optional-chaining": "^7.16.7", "@picgo/bump-version": "^1.1.2", + "@types/ali-oss": "^6.16.7", "@types/electron-devtools-installer": "^2.2.0", - "@types/fs-extra": "^9.0.13", + "@types/fs-extra": "^11.0.1", "@types/inquirer": "^6.5.0", "@types/js-yaml": "^4.0.5", "@types/lowdb": "^1.0.9", @@ -70,16 +86,16 @@ "dotenv": "^16.0.1", "electron": "^22.0.2", "electron-devtools-installer": "^3.2.0", - "eslint": "^8.31.0", + "eslint": "^8.34.0", "eslint-config-standard": ">=16.0.0", "eslint-plugin-import": "^2.24.2", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^5.1.0", - "eslint-plugin-vue": "^9.8.0", + "eslint-plugin-vue": "^9.9.0", "husky": "^3.1.0", "stylus": "^0.54.7", "stylus-loader": "^3.0.2", - "typescript": "^4.4.3", + "typescript": "^4.9.5", "vue-cli-plugin-electron-builder": "^3.0.0-alpha.4" }, "commitlint": { diff --git a/public/Upload pictures with PicGo.workflow/Contents/Info.plist b/public/Upload pictures with PicList.workflow/Contents/Info.plist similarity index 94% rename from public/Upload pictures with PicGo.workflow/Contents/Info.plist rename to public/Upload pictures with PicList.workflow/Contents/Info.plist index 6fbb08c..b3fcbe0 100644 --- a/public/Upload pictures with PicGo.workflow/Contents/Info.plist +++ b/public/Upload pictures with PicList.workflow/Contents/Info.plist @@ -14,7 +14,7 @@ NSMenuItem default - Upload pictures with PicGo + Upload pictures with PicList NSMessage runWorkflowAsService diff --git a/public/Upload pictures with PicGo.workflow/Contents/QuickLook/Thumbnail.png b/public/Upload pictures with PicList.workflow/Contents/QuickLook/Thumbnail.png similarity index 100% rename from public/Upload pictures with PicGo.workflow/Contents/QuickLook/Thumbnail.png rename to public/Upload pictures with PicList.workflow/Contents/QuickLook/Thumbnail.png diff --git a/public/Upload pictures with PicGo.workflow/Contents/Resources/background.color b/public/Upload pictures with PicList.workflow/Contents/Resources/background.color similarity index 100% rename from public/Upload pictures with PicGo.workflow/Contents/Resources/background.color rename to public/Upload pictures with PicList.workflow/Contents/Resources/background.color diff --git a/public/Upload pictures with PicGo.workflow/Contents/document.wflow b/public/Upload pictures with PicList.workflow/Contents/document.wflow similarity index 98% rename from public/Upload pictures with PicGo.workflow/Contents/document.wflow rename to public/Upload pictures with PicList.workflow/Contents/document.wflow index e83bdc6..f2eb263 100644 --- a/public/Upload pictures with PicGo.workflow/Contents/document.wflow +++ b/public/Upload pictures with PicList.workflow/Contents/document.wflow @@ -59,7 +59,7 @@ ActionParameters COMMAND_STRING - /Applications/PicGo.app/Contents/MacOS/PicGo upload "$@" > /dev/null 2>&1 & + /Applications/PicList.app/Contents/MacOS/PicList upload "$@" > /dev/null 2>&1 & CheckedForUserDefaultShell inputMethod diff --git a/public/i18n/en.yml b/public/i18n/en.yml index 874f6a1..e659e16 100644 --- a/public/i18n/en.yml +++ b/public/i18n/en.yml @@ -34,7 +34,7 @@ PICBEDS_SETTINGS: Picbeds Settings PICBEDS_MANAGE: Picbeds Manage PICLIST_SETTINGS: PicList Settings PLUGIN_SETTINGS: Plugins Settings -PICGO_SPONSOR_TEXT: PicList is a free software, if you like it, please don't forget to buy me a cup of coffee. +PICLIST_SPONSOR_TEXT: PicList is a free software, if you like it, please don't forget to buy me a cup of coffee. ALIPAY: Alipay WECHATPAY: Wechat Pay CHOOSE_PICBED: Choose Picbed @@ -88,7 +88,7 @@ SETTINGS_PLUGIN_INSTALL_MIRROR: Mirror for Plugin Install SETTINGS_CURRENT_VERSION: Current Version SETTINGS_NEWEST_VERSION: Newest Version SETTINGS_GETING: Getting... -SETTINGS_TIPS_HAS_NEW_VERSION: PicGo has a new version, please click confirm to open download page +SETTINGS_TIPS_HAS_NEW_VERSION: PicList has a new version, please click confirm to open download page SETTINGS_LOG_FILE: Log File SETTINGS_LOG_LEVEL: Log Level SETTINGS_LOG_FILE_SIZE: Log File Size @@ -191,12 +191,12 @@ UPDATE_PLUGIN: Update Plugin TIPS_NOTICE: Tips TIPS_WARNING: Warning TIPS_ERROR: Error -TIPS_INSTALL_NODE_AND_RELOAD_PICGO: Please install Node.js and restart PicGo to continue +TIPS_INSTALL_NODE_AND_RELOAD_PICGO: Please install Node.js and restart PicList to continue TIPS_PLUGIN_REMOVE_GALLERY_ITEM: Plugin is trying to remove some images from the album gallery, continue? TIPS_PLUGIN_OVERWRITE_GALLERY: Plugin is trying to overwrite the album gallery, continue? TIPS_UPLOAD_NOT_PICTURES: The latest clipboard item is not a picture -TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicGo config file broken, has been restored to default -TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicGo config file broken, has been restored to backup +TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicList config file broken, has been restored to default +TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicList config file broken, has been restored to backup TIPS_PICGO_BACKUP_FILE_VERSION: 'Backup file version: ${v}' TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: Custom config file parse error, please check the path content TIPS_SHORTCUT_MODIFIED_SUCCEED: Shortcut modified successfully diff --git a/public/i18n/zh-CN.yml b/public/i18n/zh-CN.yml index 798f154..437ffcb 100644 --- a/public/i18n/zh-CN.yml +++ b/public/i18n/zh-CN.yml @@ -34,7 +34,7 @@ PICBEDS_SETTINGS: 图床设置 PICBEDS_MANAGE: 图床管理 PICLIST_SETTINGS: PicList设置 PLUGIN_SETTINGS: 插件设置 -PICGO_SPONSOR_TEXT: PicList是免费开源的软件,如果你喜欢它,对你有帮助,可以请我喝杯蜜雪冰城~ +PICLIST_SPONSOR_TEXT: PicList是免费开源的软件,如果你喜欢它,对你有帮助,可以请我喝杯蜜雪冰城~ ALIPAY: 支付宝 WECHATPAY: 微信支付 CHOOSE_PICBED: 选择图床 @@ -88,7 +88,7 @@ SETTINGS_PLUGIN_INSTALL_MIRROR: 插件安装镜像 SETTINGS_CURRENT_VERSION: 当前版本 SETTINGS_NEWEST_VERSION: 最新版本 SETTINGS_GETING: 正在获取中 -SETTINGS_TIPS_HAS_NEW_VERSION: PicGo更新啦,请点击确定打开下载页面 +SETTINGS_TIPS_HAS_NEW_VERSION: PicList更新啦,请点击确定打开下载页面 SETTINGS_LOG_FILE: 日志文件 SETTINGS_LOG_LEVEL: 日志记录等级 SETTINGS_LOG_FILE_SIZE: 日志文件大小 @@ -191,12 +191,12 @@ UPDATE_PLUGIN: 更新插件 TIPS_NOTICE: 注意 TIPS_WARNING: 警告 TIPS_ERROR: 发生错误 -TIPS_INSTALL_NODE_AND_RELOAD_PICGO: 请安装Node.js并重启PicGo再继续操作 +TIPS_INSTALL_NODE_AND_RELOAD_PICGO: 请安装Node.js并重启PicList再继续操作 TIPS_PLUGIN_REMOVE_GALLERY_ITEM: 有插件正在试图删除一些相册图片,是否继续 TIPS_PLUGIN_OVERWRITE_GALLERY: 有插件正在试图覆盖相册列表,是否继续 TIPS_UPLOAD_NOT_PICTURES: 剪贴板最新的一条记录不是图片 -TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicGo 配置文件损坏,已经恢复为默认配置 -TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicGo 配置文件损坏,已经恢复为备份配置 +TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicList 配置文件损坏,已经恢复为默认配置 +TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicList 配置文件损坏,已经恢复为备份配置 TIPS_PICGO_BACKUP_FILE_VERSION: '备份文件版本: ${v}' TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: 自定义文件解析出错,请检查路径内容是否正确 TIPS_SHORTCUT_MODIFIED_SUCCEED: 快捷键已经修改成功 diff --git a/public/i18n/zh-TW.yml b/public/i18n/zh-TW.yml index c8787a5..0b24301 100644 --- a/public/i18n/zh-TW.yml +++ b/public/i18n/zh-TW.yml @@ -34,7 +34,7 @@ PICBEDS_SETTINGS: 圖床設定 PICBEDS_MANAGE: 圖床管理 PICLIST_SETTINGS: PicList設定 PLUGIN_SETTINGS: 插件設定 -PICGO_SPONSOR_TEXT: PicList是開放原始碼的軟體,如果你喜歡它,對你有幫助,不妨請我喝杯咖啡~ +PICLIST_SPONSOR_TEXT: PicList是開放原始碼的軟體,如果你喜歡它,對你有幫助,不妨請我喝杯咖啡~ ALIPAY: 支付寶 WECHATPAY: 微信支付 CHOOSE_PICBED: 選擇圖床 @@ -88,7 +88,7 @@ SETTINGS_PLUGIN_INSTALL_MIRROR: 插件安裝鏡像 SETTINGS_CURRENT_VERSION: 當前版本 SETTINGS_NEWEST_VERSION: 最新版本 SETTINGS_GETING: 正在取得中 -SETTINGS_TIPS_HAS_NEW_VERSION: PicGo更新啦,請點擊確定開啟下載頁面 +SETTINGS_TIPS_HAS_NEW_VERSION: PicList更新啦,請點擊確定開啟下載頁面 SETTINGS_LOG_FILE: 記錄檔案 SETTINGS_LOG_LEVEL: 記錄等级 SETTINGS_LOG_FILE_SIZE: 記錄檔案大小 @@ -191,12 +191,12 @@ UPDATE_PLUGIN: 更新插件 TIPS_NOTICE: 注意 TIPS_WARNING: 警告 TIPS_ERROR: 發生錯誤 -TIPS_INSTALL_NODE_AND_RELOAD_PICGO: 請安裝Node.js並重新啟動PicGo再繼續操作 +TIPS_INSTALL_NODE_AND_RELOAD_PICGO: 請安裝Node.js並重新啟動PicList再繼續操作 TIPS_PLUGIN_REMOVE_GALLERY_ITEM: 有插件正在試圖刪除一些相簿圖片,是否繼續? TIPS_PLUGIN_OVERWRITE_GALLERY: 有插件正在試圖覆蓋相簿列表,是否繼續? TIPS_UPLOAD_NOT_PICTURES: 剪貼簿最新的一條記錄不是圖片 -TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicGo 設定檔案已損壞,已經恢復為預設設定 -TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicGo 設定檔案已損壞,已經恢復為備份設定 +TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicList設定檔案已損壞,已經恢復為預設設定 +TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicList 設定檔案已損壞,已經恢復為備份設定 TIPS_PICGO_BACKUP_FILE_VERSION: '備份檔案版本: ${v}' TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: 自訂設定檔案解析出錯,請檢查路徑內容是否正確 TIPS_SHORTCUT_MODIFIED_SUCCEED: 快捷鍵已經修改成功 diff --git a/public/index.html b/public/index.html index 79a75a2..f2d56b5 100644 --- a/public/index.html +++ b/public/index.html @@ -6,11 +6,11 @@ - PicGo + PicList
diff --git a/public/menubar-nodarwin.png b/public/menubar-nodarwin.png index af5eaff..d6ff740 100644 Binary files a/public/menubar-nodarwin.png and b/public/menubar-nodarwin.png differ diff --git a/public/menubar-nodarwin@2x.png b/public/menubar-nodarwin@2x.png index d5c6200..d6ff740 100644 Binary files a/public/menubar-nodarwin@2x.png and b/public/menubar-nodarwin@2x.png differ diff --git a/public/menubar-nodarwin@3x.png b/public/menubar-nodarwin@3x.png index b9c4956..c0e2e97 100644 Binary files a/public/menubar-nodarwin@3x.png and b/public/menubar-nodarwin@3x.png differ diff --git a/public/picbed/aliyun.png b/public/picbed/aliyun.png new file mode 100644 index 0000000..44b9f47 Binary files /dev/null and b/public/picbed/aliyun.png differ diff --git a/public/picbed/github.png b/public/picbed/github.png new file mode 100644 index 0000000..10737d4 Binary files /dev/null and b/public/picbed/github.png differ diff --git a/public/picbed/imgur.png b/public/picbed/imgur.png new file mode 100644 index 0000000..4631b7e Binary files /dev/null and b/public/picbed/imgur.png differ diff --git a/public/picbed/qiniu.png b/public/picbed/qiniu.png new file mode 100644 index 0000000..6c86264 Binary files /dev/null and b/public/picbed/qiniu.png differ diff --git a/public/picbed/smms.png b/public/picbed/smms.png new file mode 100644 index 0000000..97bb61c Binary files /dev/null and b/public/picbed/smms.png differ diff --git a/public/picbed/tcyun.png b/public/picbed/tcyun.png new file mode 100644 index 0000000..382ad53 Binary files /dev/null and b/public/picbed/tcyun.png differ diff --git a/public/picbed/upyun.png b/public/picbed/upyun.png new file mode 100644 index 0000000..14b53da Binary files /dev/null and b/public/picbed/upyun.png differ diff --git a/scripts/config.js b/scripts/config.js index 43e6a46..f1b6d6c 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -2,24 +2,24 @@ // macos const darwin = [{ - appNameWithPrefix: 'PicGo-', + appNameWithPrefix: 'PicList-', ext: '.dmg', arch: '-arm64', 'version-file': 'latest-mac.yml' }, { - appNameWithPrefix: 'PicGo-', + appNameWithPrefix: 'PicList-', ext: '.dmg', arch: '-x64', 'version-file': 'latest-mac.yml' }] const linux = [{ - appNameWithPrefix: 'PicGo-', + appNameWithPrefix: 'PicList-', ext: '.AppImage', arch: '', 'version-file': 'latest-linux.yml' }, { - appNameWithPrefix: 'picgo_', + appNameWithPrefix: 'piclist_', ext: '.snap', arch: '_amd64', 'version-file': 'latest-linux.yml' @@ -27,17 +27,17 @@ const linux = [{ // windows const win32 = [{ - appNameWithPrefix: 'PicGo-Setup-', + appNameWithPrefix: 'PicList-Setup-', ext: '.exe', arch: '-ia32', 'version-file': 'latest.yml' }, { - appNameWithPrefix: 'PicGo-Setup-', + appNameWithPrefix: 'PicList-Setup-', ext: '.exe', arch: '-x64', 'version-file': 'latest.yml' }, { - appNameWithPrefix: 'PicGo-Setup-', + appNameWithPrefix: 'PicList-Setup-', ext: '.exe', arch: '', // 32 & 64 'version-file': 'latest.yml' diff --git a/scripts/cos-link.js b/scripts/cos-link.js index a2c0afc..667c254 100644 --- a/scripts/cos-link.js +++ b/scripts/cos-link.js @@ -1,18 +1,18 @@ const pkg = require('../package.json') const version = pkg.version // TODO: use the same name format -const generateURL = (platform, ext, prefix = 'PicGo-') => { - return `https://picgo-1251750343.cos.ap-chengdu.myqcloud.com/${version}/${prefix}${version}${platform}${ext}` +const generateURL = (platform, ext, prefix = 'PicList-') => { + return `https://release.piclist.cn/${version}/${prefix}${version}${platform}${ext}` } const platformExtList = [ - ['-arm64', '.dmg', 'PicGo-'], - ['-x64', '.dmg', 'PicGo-'], - ['', '.AppImage', 'PicGo-'], - ['-ia32', '.exe', 'PicGo-Setup-'], - ['-x64', '.exe', 'PicGo-Setup-'], - ['', '.exe', 'PicGo-Setup-'], - ['_amd64', '.snap', 'picgo_'] + ['-arm64', '.dmg', 'PicList-'], + ['-x64', '.dmg', 'PicList-'], + ['', '.AppImage', 'PicList-'], + ['-ia32', '.exe', 'PicList-Setup-'], + ['-x64', '.exe', 'PicList-Setup-'], + ['', '.exe', 'PicList-Setup-'], + ['_amd64', '.snap', 'piclist_'] ] const links = platformExtList.map(([arch, ext, prefix]) => { diff --git a/scripts/upload-dist-to-cos.js b/scripts/upload-dist-to-cos.js deleted file mode 100644 index d55fe93..0000000 --- a/scripts/upload-dist-to-cos.js +++ /dev/null @@ -1,103 +0,0 @@ -// upload dist bundled-app to cos -require('dotenv').config() -const crypto = require('crypto') -const fs = require('fs') -const mime = require('mime-types') -const pkg = require('../package.json') -const configList = require('./config') -const axios = require('axios').default -const path = require('path') -const distPath = path.join(__dirname, '../dist_electron') - -const BUCKET = 'picgo-1251750343' -// const AREA = 'ap-chengdu' -const VERSION = pkg.version -const FILE_PATH = `${VERSION}/` -const SECRET_ID = process.env.PICGO_ENV_COS_SECRET_ID -const SECRET_KEY = process.env.PICGO_ENV_COS_SECRET_KEY - -// https://cloud.tencent.com/document/product/436/7778#signature -/** - * @param {string} fileName - * @returns - */ -const generateSignature = (fileName, folder = FILE_PATH) => { - const secretKey = SECRET_KEY - // const area = AREA - const bucket = BUCKET - const path = folder - const today = Math.floor(new Date().getTime() / 1000) - const tomorrow = today + 86400 - const signTime = `${today};${tomorrow}` - const signKey = crypto.createHmac('sha1', secretKey).update(signTime).digest('hex') - const httpString = `put\n/${path}${fileName}\n\nhost=${bucket}.cos.accelerate.myqcloud.com\n` - const sha1edHttpString = crypto.createHash('sha1').update(httpString).digest('hex') - const stringToSign = `sha1\n${signTime}\n${sha1edHttpString}\n` - const signature = crypto.createHmac('sha1', signKey).update(stringToSign).digest('hex') - return { - signature, - signTime - } -} - -/** - * - * @param {string} fileName - * @param {Buffer} fileBuffer - * @param {{ signature: string, signTime: string }} signature - * @returns - */ -const getReqOptions = (fileName, fileBuffer, signature, folder = FILE_PATH) => { - return { - method: 'PUT', - url: `http://${BUCKET}.cos.accelerate.myqcloud.com/${encodeURI(folder)}${encodeURI(fileName)}`, - headers: { - Host: `${BUCKET}.cos.accelerate.myqcloud.com`, - Authorization: `q-sign-algorithm=sha1&q-ak=${SECRET_ID}&q-sign-time=${signature.signTime}&q-key-time=${signature.signTime}&q-header-list=host&q-url-param-list=&q-signature=${signature.signature}`, - contentType: mime.lookup(fileName), - useAgent: `PicGo;${pkg.version};null;null` - }, - maxContentLength: Infinity, - maxBodyLength: Infinity, - data: fileBuffer, - resolveWithFullResponse: true - } -} - -const uploadFile = async () => { - try { - const platform = process.platform - if (configList[platform]) { - let versionFileHasUploaded = false - for (const [index, config] of configList[platform].entries()) { - const fileName = `${config.appNameWithPrefix}${VERSION}${config.arch}${config.ext}` - const filePath = path.join(distPath, fileName) - const versionFilePath = path.join(distPath, config['version-file']) - let versionFileName = config['version-file'] - if (VERSION.toLocaleLowerCase().includes('beta')) { - versionFileName = versionFileName.replace('.yml', '.beta.yml') - } - // upload dist file - const signature = generateSignature(fileName) - const reqOptions = getReqOptions(fileName, fs.readFileSync(filePath), signature) - console.log('[PicGo Dist] Uploading...', fileName, `${index + 1}/${configList[platform].length}`) - await axios.request(reqOptions) - - // upload version file - if (!versionFileHasUploaded) { - const signature = generateSignature(versionFileName, '') - const reqOptions = getReqOptions(versionFileName, fs.readFileSync(versionFilePath), signature, '') - console.log('[PicGo Version File] Uploading...', versionFileName) - await axios.request(reqOptions) - versionFileHasUploaded = true - } - } - } else { - console.warn('platform not supported!', platform) - } - } catch (e) { - console.error(e) - } -} - -uploadFile() diff --git a/scripts/upload-dist-to-r2.js b/scripts/upload-dist-to-r2.js new file mode 100644 index 0000000..b5ad424 --- /dev/null +++ b/scripts/upload-dist-to-r2.js @@ -0,0 +1,67 @@ +// upload dist bundled-app to r2 +require('dotenv').config() +const S3 = require('aws-sdk/clients/s3') +const pkg = require('../package.json') +const configList = require('./config') +const fs = require('fs') +const path = require('path') + +const BUCKET = 'piclist-dl' +const VERSION = pkg.version +const FILE_PATH = `${VERSION}/` +const ACCOUNT_ID = process.env.R2_ACCOUNT_ID +const SECRET_ID = process.env.R2_SECRET_ID +const SECRET_KEY = process.env.R2_SECRET_KEY +console.log(ACCOUNT_ID, SECRET_ID, SECRET_KEY) + +const s3 = new S3({ + endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`, + accessKeyId: SECRET_ID, + secretAccessKey: SECRET_KEY, + signatureVersion: 'v4', +}) + +const uploadFile = async () => { + try { + const platform = process.platform + if (configList[platform]) { + let versionFileHasUploaded = false + for (const [index, config] of configList[platform].entries()) { + const fileName = `${config.appNameWithPrefix}${VERSION}${config.arch}${config.ext}` + const distPath = path.join(__dirname, '../dist_electron') + let versionFileName = config['version-file'] + console.log('[PicList Dist] Uploading...', fileName, `${index + 1}/${configList[platform].length}`) + const fileBuffer = fs.readFileSync(path.join(distPath, fileName)) + await s3.upload({ + Bucket: BUCKET, + Key: `${FILE_PATH}${fileName}`, + Body: fileBuffer + }).promise() + // upload version file + if (!versionFileHasUploaded) { + console.log('[PicList Version File] Uploading...', versionFileName) + let versionFilePath + if (platform === 'win32') { + versionFilePath = path.join(distPath, 'latest.yml') + } else if (platform === 'darwin') { + versionFilePath = path.join(distPath, 'latest-mac.yml') + } else { + versionFilePath = path.join(distPath, 'latest-linux.yml') + } + const versionFileBuffer = fs.readFileSync(versionFilePath) + await s3.upload({ + Bucket: BUCKET, + Key: `${versionFileName}`, + Body: versionFileBuffer + }).promise() + versionFileHasUploaded = true + } + } + } else { + console.warn('platform not supported!', platform) + } + } catch (err) { + console.error(err) + } +} +uploadFile() \ No newline at end of file diff --git a/src/background.ts b/src/background.ts index bec85b2..c8b4acb 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,23 +1,3 @@ import { bootstrap } from '~/main/lifeCycle' bootstrap.launchApp() - -/** - * Auto Updater - * - * Uncomment the following code below and install `electron-updater` to - * support auto updating. Code Signing with a valid certificate is required. - * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating -*/ - -// import { autoUpdater } from 'electron-updater' - -// autoUpdater.on('update-downloaded', () => { -// autoUpdater.quitAndInstall() -// }) - -// app.on('ready', () => { -// if (process.env.NODE_ENV === 'production') { -// autoUpdater.checkForUpdates() -// } -// }) diff --git a/src/main.ts b/src/main.ts index 3b99a1c..337fd94 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { webFrame } from 'electron' import VueLazyLoad from 'vue3-lazyload' import axios from 'axios' import { mainMixin } from './renderer/utils/mainMixin' +import ContextMenu from '@imengyu/vue3-context-menu' import { dragMixin } from '@/utils/mixin' import { initTalkingData } from './renderer/utils/analytics' import db from './renderer/utils/db' @@ -15,6 +16,8 @@ import { getConfig, saveConfig, sendToMain, triggerRPC } from '@/utils/dataSende import { store } from '@/store' import vue3PhotoPreview from 'vue3-photo-preview' import 'vue3-photo-preview/dist/index.css' +import { createPinia } from 'pinia' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' webFrame.setVisualZoomLevelLimits(1, 1) @@ -45,6 +48,8 @@ app.config.globalProperties.sendToMain = sendToMain app.mixin(mainMixin) app.mixin(dragMixin) +const pinia = createPinia() +pinia.use(piniaPluginPersistedstate) app.use(VueLazyLoad, { error: `file://${__static.replace(/\\/g, '/')}/unknown-file-type.svg` @@ -53,7 +58,8 @@ app.use(ElementUI) app.use(router) app.use(store) app.use(vue3PhotoPreview) - +app.use(pinia) +app.use(ContextMenu) app.mount('#app') initTalkingData() diff --git a/src/main/apis/app/remoteNotice/index.ts b/src/main/apis/app/remoteNotice/index.ts index 9a7db6b..521f5ae 100644 --- a/src/main/apis/app/remoteNotice/index.ts +++ b/src/main/apis/app/remoteNotice/index.ts @@ -9,12 +9,11 @@ import path from 'path' import axios from 'axios' import windowManager from '../window/windowManager' import { showNotification } from '~/main/utils/common' -import { isDev } from '~/universal/utils/common' // for test -const REMOTE_NOTICE_URL = isDev ? 'http://localhost:8181/remote-notice.json' : 'https://picgo-1251750343.cos.accelerate.myqcloud.com/remote-notice.yml' +const REMOTE_NOTICE_URL = 'https://release.piclist.cn/remote-notice.json' -const REMOTE_NOTICE_LOCAL_STORAGE_FILE = 'picgo-remote-notice.json' +const REMOTE_NOTICE_LOCAL_STORAGE_FILE = 'piclist-remote-notice.json' const STORE_PATH = app.getPath('userData') @@ -106,7 +105,6 @@ class RemoteNoticeHandler { if (this.checkActionCount(action)) { switch (action.type) { case IRemoteNoticeActionType.SHOW_DIALOG: { - // SHOW DIALOG const currentWindow = windowManager.getAvailableWindow() dialog.showOpenDialog(currentWindow, action.data?.options) break diff --git a/src/main/apis/app/system/index.ts b/src/main/apis/app/system/index.ts index 6c1919c..a03ed5e 100644 --- a/src/main/apis/app/system/index.ts +++ b/src/main/apis/app/system/index.ts @@ -181,7 +181,6 @@ export function createTray () { } } else { const imgUrl = img.toDataURL() - // console.log(imgUrl) obj.push({ width: img.getSize().width, height: img.getSize().height, diff --git a/src/main/apis/app/uploader/apis.ts b/src/main/apis/app/uploader/apis.ts index fd9efd3..e3d5350 100644 --- a/src/main/apis/app/uploader/apis.ts +++ b/src/main/apis/app/uploader/apis.ts @@ -10,7 +10,6 @@ import db, { GalleryDB } from '~/main/apis/core/datastore' import { handleCopyUrl } from '~/main/utils/common' import { handleUrlEncode } from '#/utils/common' import { T } from '~/main/i18n/index' -// import dayjs from 'dayjs' const handleClipboardUploading = async (): Promise => { const useBuiltinClipboard = !!db.get('settings.useBuiltinClipboard') diff --git a/src/main/apis/app/uploader/index.ts b/src/main/apis/app/uploader/index.ts index 667bcd6..34e6078 100644 --- a/src/main/apis/app/uploader/index.ts +++ b/src/main/apis/app/uploader/index.ts @@ -11,7 +11,7 @@ import db from '~/main/apis/core/datastore' import windowManager from 'apis/app/window/windowManager' import { IWindowList } from '#/types/enum' import util from 'util' -import { IPicGo } from 'picgo' +import { IPicGo } from 'piclist' import { showNotification, calcDurationRange, getClipboardFilePath } from '~/main/utils/common' import { RENAME_FILE_NAME, TALKING_DATA_EVENT } from '~/universal/events/constants' import logger from '@core/picgo/logger' @@ -163,6 +163,9 @@ class Uploader { duration: Date.now() - startTime } as IAnalyticsData) } + output.forEach((item: ImgInfo) => { + item.config = db.get(`picBed.${item.type}`) + }) return output.filter(item => item.imgUrl) } else { return false diff --git a/src/main/apis/app/window/windowList.ts b/src/main/apis/app/window/windowList.ts index 99e99a7..3ba74dd 100644 --- a/src/main/apis/app/window/windowList.ts +++ b/src/main/apis/app/window/windowList.ts @@ -11,17 +11,10 @@ import db from '~/main/apis/core/datastore' import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants' import { app } from 'electron' import { remoteNoticeHandler } from '../remoteNotice' -// import { i18n } from '~/main/i18n' -// import { URLSearchParams } from 'url' const windowList = new Map() const handleWindowParams = (windowURL: string) => { - // const [baseURL, hash = ''] = windowURL.split('#') - // const search = new URLSearchParams() - // const lang = i18n.getLanguage() - // search.append('lang', lang) - // return `${baseURL}?${search.toString()}#${hash}` return windowURL } diff --git a/src/main/apis/app/window/windowManager.ts b/src/main/apis/app/window/windowManager.ts index 5aa38a2..d5f000f 100644 --- a/src/main/apis/app/window/windowManager.ts +++ b/src/main/apis/app/window/windowManager.ts @@ -45,14 +45,6 @@ class WindowManager implements IWindowManager { return this.windowMap.has(name) } - // useless - // delete (name: IWindowList) { - // const window = this.windowMap.get(name) - // if (window) { - // this.windowIdMap.delete(window.id) - // this.windowMap.delete(name) - // } - // } deleteById = (id: number) => { const name = this.windowIdMap.get(id) if (name) { diff --git a/src/main/apis/core/datastore/dbChecker.ts b/src/main/apis/core/datastore/dbChecker.ts index 9d7bdc4..6ab6fb0 100644 --- a/src/main/apis/core/datastore/dbChecker.ts +++ b/src/main/apis/core/datastore/dbChecker.ts @@ -1,11 +1,11 @@ import fs from 'fs-extra' import writeFile from 'write-file-atomic' import path from 'path' -import { app as APP } from 'electron' -import { getLogger } from '@core/utils/localLogger' +import { app } from 'electron' +import { getLogger } from '../utils/localLogger' import dayjs from 'dayjs' import { T } from '~/main/i18n' -const STORE_PATH = APP.getPath('userData') +const STORE_PATH = app.getPath('userData') const configFilePath = path.join(STORE_PATH, 'data.json') const configFileBackupPath = path.join(STORE_PATH, 'data.bak.json') export const defaultConfigPath = configFilePath @@ -79,7 +79,6 @@ function dbPathChecker (): string { if (_configFilePath) { return _configFilePath } - // defaultConfigPath _configFilePath = defaultConfigPath // if defaultConfig path is not exit // do not parse the content of config @@ -98,8 +97,8 @@ function dbPathChecker (): string { } return _configFilePath } catch (e) { - const picgoLogPath = path.join(STORE_PATH, 'picgo-gui-local.log') - const logger = getLogger(picgoLogPath) + const piclistLogPath = path.join(STORE_PATH, 'piclist-gui-local.log') + const logger = getLogger(piclistLogPath, 'PicList') if (!hasCheckPath) { const optionsTpl = { title: T('TIPS_NOTICE'), @@ -123,8 +122,8 @@ function getGalleryDBPath (): { dbBackupPath: string } { const configPath = dbPathChecker() - const dbPath = path.join(path.dirname(configPath), 'picgo.db') - const dbBackupPath = path.join(path.dirname(dbPath), 'picgo.bak.db') + const dbPath = path.join(path.dirname(configPath), 'piclist.db') + const dbBackupPath = path.join(path.dirname(dbPath), 'piclist.bak.db') return { dbPath, dbBackupPath diff --git a/src/main/apis/core/picgo/index.ts b/src/main/apis/core/picgo/index.ts index e9d5d1b..6cf521a 100644 --- a/src/main/apis/core/picgo/index.ts +++ b/src/main/apis/core/picgo/index.ts @@ -1,6 +1,6 @@ import { dbChecker, dbPathChecker } from 'apis/core/datastore/dbChecker' import pkg from 'root/package.json' -import { PicGo } from 'picgo' +import { PicGo } from 'piclist' import db from 'apis/core/datastore' import debounce from 'lodash/debounce' diff --git a/src/main/apis/core/utils/localLogger.ts b/src/main/apis/core/utils/localLogger.ts index 0083edd..3cd5518 100644 --- a/src/main/apis/core/utils/localLogger.ts +++ b/src/main/apis/core/utils/localLogger.ts @@ -41,9 +41,9 @@ const recreateLogFile = (logPath: string): void => { } /** - * for local log before picgo inited + * for local log before piclist inited */ -const getLogger = (logPath: string) => { +const getLogger = (logPath: string, logtype: string) => { let hasUncathcedError = false try { if (!fs.existsSync(logPath)) { @@ -64,7 +64,7 @@ const getLogger = (logPath: string) => { return } try { - let log = `${dayjs().format('YYYY-MM-DD HH:mm:ss')} [PicGo ${type.toUpperCase()}] ` + let log = `${dayjs().format('YYYY-MM-DD HH:mm:ss')} [${logtype} ${type.toUpperCase()}] ` msg.forEach((item: ILogArgvTypeWithError) => { if (typeof item === 'object' && type === 'error') { log += `\n------Error Stack Begin------\n${util.format(item.stack)}\n-------Error Stack End------- ` diff --git a/src/main/events/picgoCoreIPC.ts b/src/main/events/picgoCoreIPC.ts index de70b21..16ebf53 100644 --- a/src/main/events/picgoCoreIPC.ts +++ b/src/main/events/picgoCoreIPC.ts @@ -11,7 +11,7 @@ import { IPasteStyle, IPicGoHelperType, IWindowList } from '#/types/enum' import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler' import picgo from '@core/picgo' import { handleStreamlinePluginName, simpleClone } from '~/universal/utils/common' -import { IGuiMenuItem, PicGo as PicGoCore } from 'picgo' +import { IGuiMenuItem, PicGo as PicGoCore } from 'piclist' import windowManager from 'apis/app/window/windowManager' import { showNotification } from '~/main/utils/common' import { dbPathChecker } from 'apis/core/datastore/dbChecker' diff --git a/src/main/events/remotes/menu.ts b/src/main/events/remotes/menu.ts index be5605b..4e0457f 100644 --- a/src/main/events/remotes/menu.ts +++ b/src/main/events/remotes/menu.ts @@ -11,7 +11,7 @@ import pkg from 'root/package.json' import GuiApi from 'apis/gui' import { PICGO_CONFIG_PLUGIN, PICGO_HANDLE_PLUGIN_DONE, PICGO_HANDLE_PLUGIN_ING, PICGO_TOGGLE_PLUGIN, SHOW_MAIN_PAGE_DONATION, SHOW_MAIN_PAGE_QRCODE } from '~/universal/events/constants' import picgoCoreIPC from '~/main/events/picgoCoreIPC' -import { PicGo as PicGoCore } from 'picgo' +import { PicGo as PicGoCore } from 'piclist' import { T } from '~/main/i18n' import { changeCurrentUploader } from '~/main/utils/handleUploaderConfig' diff --git a/src/main/lifeCycle/errorHandler.ts b/src/main/lifeCycle/errorHandler.ts index 38a7e3b..38b3b7e 100644 --- a/src/main/lifeCycle/errorHandler.ts +++ b/src/main/lifeCycle/errorHandler.ts @@ -2,9 +2,9 @@ import path from 'path' import { app } from 'electron' import { getLogger } from 'apis/core/utils/localLogger' const STORE_PATH = app.getPath('userData') -const LOG_PATH = path.join(STORE_PATH, 'picgo-gui-local.log') +const LOG_PATH = path.join(STORE_PATH, 'piclist-gui-local.log') -const logger = getLogger(LOG_PATH) +const logger = getLogger(LOG_PATH, 'PicList') // since the error may occur in picgo-core // so we can't use the log from picgo diff --git a/src/main/lifeCycle/fixPath.ts b/src/main/lifeCycle/fixPath.ts index ec6526f..0aa5b10 100644 --- a/src/main/lifeCycle/fixPath.ts +++ b/src/main/lifeCycle/fixPath.ts @@ -1,8 +1,8 @@ // TODO: so how to import pure esm module in electron main process????? help wanted // just copy the fix-path because I can't import pure ESM module in electron main process - -const shellPath = require('shell-path') +// @ts-nocheck +import { shellPath } from 'shell-path' export default function fixPath () { if (process.platform === 'win32') { diff --git a/src/main/lifeCycle/index.ts b/src/main/lifeCycle/index.ts index add21c1..1ecc7df 100644 --- a/src/main/lifeCycle/index.ts +++ b/src/main/lifeCycle/index.ts @@ -34,9 +34,12 @@ import bus from '@core/bus' import logger from 'apis/core/picgo/logger' import picgo from 'apis/core/picgo' import fixPath from './fixPath' +import { clearTempFolder } from '../manage/utils/common' import { initI18n } from '~/main/utils/handleI18n' import { remoteNoticeHandler } from 'apis/app/remoteNotice' - +import { manageIpcList } from '../manage/events/ipcList' +import getManageApi from '../manage/Main' +import UpDownTaskQueue from '../manage/datastore/upDownTaskQueue' const isDevelopment = process.env.NODE_ENV !== 'production' const handleStartUpFiles = (argv: string[], cwd: string) => { @@ -64,6 +67,9 @@ class LifeCycle { beforeOpen() initI18n() ipcList.listen() + getManageApi() + UpDownTaskQueue.getInstance() + manageIpcList.listen() busEventList.listen() updateShortKeyFromVersion212(db, db.get('settings.shortKey')) await migrateGalleryFromVersion230(db, GalleryDB.getInstance(), picgo) @@ -135,7 +141,7 @@ class LifeCycle { openAtLogin: db.get('settings.autoStart') || false }) if (process.platform === 'win32') { - app.setAppUserModelId('com.molunerfinn.picgo') + app.setAppUserModelId('com.kuingsmile.piclist') } if (process.env.XDG_CURRENT_DESKTOP && process.env.XDG_CURRENT_DESKTOP.includes('Unity')) { @@ -151,6 +157,8 @@ class LifeCycle { }) app.on('will-quit', () => { + UpDownTaskQueue.getInstance().persist() + clearTempFolder() globalShortcut.unregisterAll() bus.removeAllListeners() server.shutdown() diff --git a/src/main/manage/Main.ts b/src/main/manage/Main.ts new file mode 100644 index 0000000..e2419ef --- /dev/null +++ b/src/main/manage/Main.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +import { manageDbChecker } from './datastore/dbChecker' +import { ManageApi } from './manageApi' + +manageDbChecker() +const getManageApi = (picBedName: string = 'placeholder'): ManageApi => { + return new ManageApi(picBedName) +} + +export default getManageApi diff --git a/src/main/manage/apis/aliyun.ts b/src/main/manage/apis/aliyun.ts new file mode 100644 index 0000000..9ce1ee7 --- /dev/null +++ b/src/main/manage/apis/aliyun.ts @@ -0,0 +1,587 @@ +import axios from 'axios' +import { hmacSha1Base64, getFileMimeType, gotDownload, formatError } from '../utils/common' +import { ipcMain, IpcMainEvent } from 'electron' +import fs from 'fs-extra' +import { XMLParser } from 'fast-xml-parser' +import OSS from 'ali-oss' +import path from 'path' +import { isImage } from '~/renderer/manage/utils/common' +import windowManager from 'apis/app/window/windowManager' +import { IWindowList } from '#/types/enum' +import UpDownTaskQueue, +{ + uploadTaskSpecialStatus, + commonTaskStatus +} from '../datastore/upDownTaskQueue' +import { ManageLogger } from '../utils/logger' + +// 坑爹阿里云 返回数据类型标注和实际各种不一致 +class AliyunApi { + ctx: OSS + accessKeyId: string + accessKeySecret: string + timeOut = 60000 + logger: ManageLogger + + constructor (accessKeyId: string, accessKeySecret: string, logger: ManageLogger) { + this.ctx = new OSS({ + accessKeyId, + accessKeySecret, + secure: true + }) + this.accessKeyId = accessKeyId + this.accessKeySecret = accessKeySecret + this.logger = logger + } + + formatFolder (item: string, slicedPrefix: string) { + return { + key: item, + fileSize: 0, + formatedTime: '', + fileName: item.replace(slicedPrefix, '').replace('/', ''), + isDir: true, + checked: false, + isImage: false, + match: false, + Key: item + } + } + + formatFile (item: OSS.ObjectMeta, slicedPrefix: string, urlPrefix: string): any { + const result = { + ...item, + key: item.name, + rawUrl: `${urlPrefix}/${item.name}`, + fileName: item.name.replace(slicedPrefix, ''), + fileSize: item.size, + formatedTime: new Date(item.lastModified).toLocaleString(), + isDir: false, + checked: false, + match: false, + isImage: isImage(item.name.replace(slicedPrefix, '')) + } + const temp = result.rawUrl + result.rawUrl = result.url + result.url = temp + return result + } + + getCanonicalizedOSSHeaders (headers: IStringKeyMap) { + const lowerCaseHeaders = Object.keys(headers).reduce((acc, key) => { + acc[key.toLowerCase()] = headers[key] + return acc + }, {} as IStringKeyMap) + let canonicalizedOSSHeaders = '' + const headerKeys = Object.keys(lowerCaseHeaders).sort() + headerKeys.forEach((key) => { + key.startsWith('x-oss-') && (canonicalizedOSSHeaders += `${key}:${lowerCaseHeaders[key]}\n`) + }) + return canonicalizedOSSHeaders + } + + authorization (method: string, canonicalizedResource: string, headers: IStringKeyMap, contentMd5: string, contentType: string) { + const date = new Date().toUTCString() + const stringToSign = `${method.toUpperCase()}\n${contentMd5}\n${contentType}\n${date}\n${this.getCanonicalizedOSSHeaders(headers)}${canonicalizedResource}` + return `OSS ${this.accessKeyId}:${hmacSha1Base64(this.accessKeySecret, stringToSign)}` + } + + getNewCtx (region: string, bucket: string) { + return new OSS({ + accessKeyId: this.accessKeyId, + accessKeySecret: this.accessKeySecret, + region, + bucket, + secure: true + }) + } + + /** + * 获取存储桶列表 + */ + async getBucketList (): Promise { + const formatItem = (item: OSS.Bucket) => { + return { + Name: item.name, + Location: item.region, + CreationDate: item.creationDate + } + } + const res = await this.ctx.listBuckets({ + 'max-keys': 1000 + }) as IStringKeyMap + const result = [] as IStringKeyMap[] + let NextMarker = '' + if (res.res.statusCode === 200) { + if (res.buckets) { + result.push(...res.buckets.map((item: OSS.Bucket) => formatItem(item))) + let isTruncated = res.isTruncated + NextMarker = res.nextMarker + while (isTruncated) { + const res = await this.ctx.listBuckets({ + marker: NextMarker, + 'max-keys': 1000 + }) as IStringKeyMap + if (res.res.statusCode === 200) { + if (res.buckets) { + result.push(...res.buckets.map((item: OSS.Bucket) => formatItem(item))) + isTruncated = res.isTruncated + NextMarker = res.nextMarker + } else { + isTruncated = false + } + } else { + isTruncated = false + } + } + return result + } else { + return [] + } + } else { + return [] + } + } + + /** + * 获取自定义域名 + */ + async getBucketDomain (param: IStringKeyMap): Promise { + const headers = { + Date: new Date().toUTCString() + } + const authorization = this.authorization('GET', `/${param.bucketName}/?cname`, headers, '', '') + const res = await axios({ + url: `https://${param.bucketName}.${param.region}.aliyuncs.com/?cname`, + method: 'GET', + headers: { + ...headers, + Authorization: authorization + } + }) + if (res.status === 200) { + const parser = new XMLParser() + const result = parser.parse(res.data) + if (result.ListCnameResult && result.ListCnameResult.Cname) { + if (Array.isArray(result.ListCnameResult.Cname)) { + const cnameList = [] as string[] + result.ListCnameResult.Cname.forEach((item: IStringKeyMap) => { + item.Status === 'Enabled' && cnameList.push(item.Domain) + }) + return cnameList + } else { + return result.ListCnameResult.Cname.Status === 'Enabled' ? [result.ListCnameResult.Cname.Domain] : [] + } + } else { + return [] + } + } else { + return [] + } + } + + /** + * 创建存储桶 + * @param {Object} configMap + * configMap = { + * BucketName: string, + * region: string, + * acl: string + * } + * @description + * acl: private | publicRead | publicReadWrite + */ + async createBucket (configMap: IStringKeyMap): Promise { + const client = new OSS({ + accessKeyId: this.accessKeyId, + accessKeySecret: this.accessKeySecret, + region: configMap.region, + secure: true + }) + const aclTransMap: IStringKeyMap = { + private: 'private', + publicRead: 'public-read', + publicReadWrite: 'public-read-write' + } + const res = await client.putBucket(configMap.BucketName, { + acl: aclTransMap[configMap.acl], + storageClass: 'Standard', + dataRedundancyType: 'LRS', + timeout: this.timeOut + }) + return res && res.res.status === 200 + } + + async getBucketListBackstage (configMap: IStringKeyMap): Promise { + const window = windowManager.get(IWindowList.SETTING_WINDOW)! + const { bucketName: bucket, bucketConfig: { Location: region }, prefix, cancelToken } = configMap + const slicedPrefix = prefix.slice(1) + const urlPrefix = configMap.customUrl || `https://${bucket}.${region}.aliyuncs.com` + let marker + const cancelTask = [false] + ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { + if (token === cancelToken) { + cancelTask[0] = true + ipcMain.removeAllListeners('cancelLoadingFileList') + } + }) + let res = {} as any + const result = { + fullList: [], + success: false, + finished: false + } + const client = this.getNewCtx(region, bucket) + do { + res = await client.listV2({ + prefix: slicedPrefix === '' ? undefined : slicedPrefix, + delimiter: '/', + 'max-keys': '1000', + 'continuation-token': marker + }, { + timeout: this.timeOut + }) + if (res && res.res.statusCode === 200) { + res.prefixes && res.prefixes.forEach((item: string) => { + result.fullList.push(this.formatFolder(item, slicedPrefix)) + }) + res.objects && res.objects.forEach((item: OSS.ObjectMeta) => { + item.size !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) + }) + window.webContents.send('refreshFileTransferList', result) + } else { + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + return + } + marker = res.nextContinuationToken + } while (res.isTruncated === true && !cancelTask[0]) + result.success = true + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + } + + /** + * 获取文件列表 + * @param {Object} configMap + * configMap = { + * bucketName: string, + * bucketConfig: { + * Location: string + * }, + * paging: boolean, + * prefix: string, + * marker: string, + * itemsPerPage: number, + * customUrl: string + * } + */ + async getBucketFileList (configMap: IStringKeyMap): Promise { + const { bucketName: bucket, bucketConfig: { Location: region }, prefix, marker, itemsPerPage } = configMap + const slicedPrefix = prefix.slice(1) + const urlPrefix = configMap.customUrl || `https://${bucket}.${region}.aliyuncs.com` + let res = {} as any + const result = { + fullList: [], + isTruncated: false, + nextMarker: '', + success: false + } + const client = this.getNewCtx(region, bucket) + res = await client.listV2({ + prefix: slicedPrefix === '' ? undefined : slicedPrefix, + delimiter: '/', + 'max-keys': itemsPerPage.toString(), + 'continuation-token': marker + }, { + timeout: this.timeOut + }) as any + // prefixes can be null + // objects will be [] when no file + if (res && res.res.statusCode === 200) { + res.prefixes && res.prefixes.forEach((item: string) => { + result.fullList.push(this.formatFolder(item, slicedPrefix)) + }) + res.objects && res.objects.forEach((item: OSS.ObjectMeta) => { + item.size !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) + }) + result.isTruncated = res.isTruncated + result.nextMarker = res.nextContinuationToken === null ? '' : res.nextContinuationToken + result.success = true + return result + } else { + return result + } + } + + /** + * 重命名文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * oldKey: string, + * newKey: string + * } + */ + async renameBucketFile (configMap: IStringKeyMap): Promise { + const { bucketName, region, oldKey, newKey } = configMap + const client = this.getNewCtx(region, bucketName) + const res = await client.copy( + newKey, + oldKey + ) as any + if (res && res.res.statusCode === 200) { + const res2 = await client.delete(oldKey) as any + return res2 && res2.res.statusCode === 204 + } else { + return false + } + } + + /** + * 删除文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string + * } + */ + async deleteBucketFile (configMap: IStringKeyMap): Promise { + const { bucketName, region, key } = configMap + const client = this.getNewCtx(region, bucketName) + const res = await client.delete(key) as any + return res && res.res.statusCode === 204 + } + + /** + * 删除文件夹 + * @param configMap + */ + async deleteBucketFolder (configMap: IStringKeyMap): Promise { + const { bucketName, region, key } = configMap + const client = this.getNewCtx(region, bucketName) + let marker + let isTruncated + const allFileList = { + CommonPrefixes: [] as any[], + Contents: [] as any[] + } + let res = await client.listV2({ + prefix: key, + delimiter: '/', + 'max-keys': '1000' + }, { + timeout: 60000 + }) as any + if (res && res.res.statusCode === 200) { + res.prefixes !== null && allFileList.CommonPrefixes.push(...res.prefixes) + res.objects.length > 0 && allFileList.Contents.push(...res.objects) + isTruncated = res.isTruncated + marker = res.nextContinuationToken + while (isTruncated) { + res = await client.listV2({ + prefix: key, + delimiter: '/', + 'max-keys': '1000', + 'continuation-token': marker + }, { + timeout: this.timeOut + }) as any + if (res && res.res.statusCode === 200) { + res.prefixes !== null && allFileList.CommonPrefixes.push(...res.prefixes) + res.objects.length > 0 && allFileList.Contents.push(...res.objects) + isTruncated = res.isTruncated + marker = res.nextContinuationToken + } else { + return false + } + } + } else { + return false + } + if (allFileList.CommonPrefixes.length > 0) { + for (const item of allFileList.CommonPrefixes) { + res = await this.deleteBucketFolder({ + bucketName, + region, + key: item + }) + if (!res) { + return false + } + } + } + if (allFileList.Contents.length > 0) { + const cycle = Math.ceil(allFileList.Contents.length / 1000) + for (let i = 0; i < cycle; i++) { + res = await client.deleteMulti( + allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => { + return item.name + }) + ) as any + if (!(res && res.res.statusCode === 200)) { + return false + } + } + } + return true + } + + /** + * 获取预签名url + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string, + * expires: number, + * customUrl: string + * } + */ + async getPreSignedUrl (configMap: IStringKeyMap): Promise { + const { bucketName, region, key, expires, customUrl } = configMap + const client = this.getNewCtx(region, bucketName) + const res = client.signatureUrl(key, { + expires: expires || 3600 + }) + return customUrl ? `${customUrl.replace(/\/$/, '')}/${key}${res.slice(res.indexOf('?'))}` : res + } + + /** + * 上传文件 + * @param configMap + */ + async uploadBucketFile (configMap: IStringKeyMap): Promise { + const { fileArray } = configMap + // fileArray = [{ + // bucketName: string, + // region: string, + // key: string, + // filePath: string + // fileSize: number + // }] + const instance = UpDownTaskQueue.getInstance() + fileArray.forEach((item: any) => { + item.key.startsWith('/') && (item.key = item.key.slice(1)) + }) + for (const item of fileArray) { + const { bucketName, region, key, filePath, fileName } = item + const client = this.getNewCtx(region, bucketName) + const id = `${bucketName}-${region}-${key}-${filePath}` + if (instance.getUploadTask(id)) { + continue + } + instance.addUploadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + sourceFilePath: filePath, + targetFilePath: key, + targetFileBucket: bucketName, + targetFileRegion: region + }) + client.multipartUpload( + key, + filePath, + { + partSize: 1 * 1024 * 1024, + mime: getFileMimeType(fileName), + progress: (p: number) => { + const id = `${bucketName}-${region}-${key}-${filePath}` + instance.updateUploadTask({ + id, + progress: Math.floor(p * 100), + status: uploadTaskSpecialStatus.uploading + }) + }, + timeout: 60000 + } + ).then((res: any) => { + const id = `${bucketName}-${region}-${key}-${filePath}` + if (res && res.res.statusCode === 200) { + instance.updateUploadTask({ + id, + progress: 100, + status: uploadTaskSpecialStatus.uploaded, + response: JSON.stringify(res), + finishTime: new Date().toLocaleString() + }) + } else { + instance.updateUploadTask({ + id, + progress: 0, + status: commonTaskStatus.failed, + response: JSON.stringify(res), + finishTime: new Date().toLocaleString() + }) + } + }).catch((err: any) => { + this.logger.error(formatError(err, { class: 'AliyunApi', method: 'uploadBucketFile' })) + const id = `${bucketName}-${region}-${key}-${filePath}` + instance.updateUploadTask({ + id, + progress: 0, + status: commonTaskStatus.failed, + response: JSON.stringify(err), + finishTime: new Date().toLocaleString() + }) + }) + } + return true + } + + /** + * 新建文件夹 + * @param configMap + */ + async createBucketFolder (configMap: IStringKeyMap): Promise { + const { bucketName, region, key } = configMap + const client = this.getNewCtx(region, bucketName) + const res = await client.put(key, Buffer.from('')) as any + return res && res.res.statusCode === 200 + } + + /** + * 下载文件 + * @param configMap + */ + async downloadBucketFile (configMap: IStringKeyMap): Promise { + const { downloadPath, fileArray } = configMap + // fileArray = [{ + // bucketName: string, + // region: string, + // key: string, + // fileName: string + // }] + const instance = UpDownTaskQueue.getInstance() + for (const item of fileArray) { + const { bucketName, region, key, fileName } = item + const client = this.getNewCtx(region, bucketName) + const savedFilePath = path.join(downloadPath, fileName) + const fileStream = fs.createWriteStream(savedFilePath) + const id = `${bucketName}-${region}-${key}` + if (instance.getDownloadTask(id)) { + continue + } + instance.addDownloadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + targetFilePath: savedFilePath + }) + const preSignedUrl = client.signatureUrl(key, { + expires: 60 * 60 * 48 + }) + gotDownload(instance, preSignedUrl, fileStream, id, savedFilePath, this.logger) + } + return true + } +} + +export default AliyunApi diff --git a/src/main/manage/apis/api.ts b/src/main/manage/apis/api.ts new file mode 100644 index 0000000..fabfb14 --- /dev/null +++ b/src/main/manage/apis/api.ts @@ -0,0 +1,17 @@ +import TcyunApi from './tcyun' +import AliyunApi from './aliyun' +import QiniuApi from './qiniu' +import UpyunApi from './upyun' +import SmmsApi from './smms' +import GithubApi from './github' +import ImgurApi from './imgur' + +export default { + TcyunApi, + AliyunApi, + QiniuApi, + UpyunApi, + SmmsApi, + GithubApi, + ImgurApi +} diff --git a/src/main/manage/apis/github.ts b/src/main/manage/apis/github.ts new file mode 100644 index 0000000..877f06b --- /dev/null +++ b/src/main/manage/apis/github.ts @@ -0,0 +1,436 @@ +import got from 'got' +import { ManageLogger } from '../utils/logger' +import { isImage } from '~/renderer/manage/utils/common' +import windowManager from 'apis/app/window/windowManager' +import { IWindowList } from '#/types/enum' +import { ipcMain, IpcMainEvent } from 'electron' +import { gotUpload, trimPath, gotDownload, getAgent, getOptions } from '../utils/common' +import UpDownTaskQueue, +{ + commonTaskStatus +} from '../datastore/upDownTaskQueue' +import fs from 'fs-extra' +import path from 'path' + +class GithubApi { + token: string + username: string + logger: ManageLogger + proxy: any + baseUrl = 'https://api.github.com' + commonHeaders : IStringKeyMap + + constructor (token: string, username: string, proxy: string | undefined, logger: ManageLogger) { + this.logger = logger + this.token = token.startsWith('Bearer ') ? token : `Bearer ${token}`.trim() + this.username = username + this.proxy = proxy + this.commonHeaders = { + Authorization: this.token, + Accept: 'application/vnd.github+json' + } + } + + formatFolder (item: any, slicedPrefix: string) { + let key = '' + if (slicedPrefix === '') { + key = `${item.path}/` + } else { + key = `${slicedPrefix}/${item.path}/` + } + return { + ...item, + Key: key, + key, + fileSize: 0, + formatedTime: '', + fileName: item.path, + isDir: true, + checked: false, + isImage: false, + match: false + } + } + + formatFile (item: any, slicedPrefix: string, branch: string, repo: string, cdnUrl: string | undefined) { + let rawUrl = '' + if (cdnUrl) { + const placeholder = ['{username}', '{repo}', '{branch}', '{path}'] + if (placeholder.some(item => cdnUrl.includes(item))) { + rawUrl = cdnUrl.replace('{username}', this.username) + .replace('{repo}', repo) + .replace('{branch}', branch) + .replace('{path}', `${slicedPrefix}/${item.path}`) + } else { + rawUrl = `${cdnUrl}/${slicedPrefix}/${item.path}` + } + } else { + rawUrl = `https://raw.githubusercontent.com/${this.username}/${repo}/${branch}/${slicedPrefix}/${item.path}` + } + rawUrl = rawUrl.replace(/(? { + let initPage = 1 + let res + const result = [] as any[] + do { + res = await got( + `${this.baseUrl}/user/repos`, + getOptions('GET', this.commonHeaders, { page: initPage, per_page: 100 }, 'json', undefined, undefined, this.proxy) + ) as any + if (res.statusCode === 200) { + res.body.forEach((item: any) => { + result.push({ + ...item, + Name: item.name, + Location: item.id, + CreationDate: item.created_at + }) + }) + } else { + return [] + } + initPage++ + } while (res.body.length > 0) + return result + } + + /** + * 获取branch列表 + */ + async getBucketDomain (param: IStringKeyMap): Promise { + const { bucketName: repo } = param + let initPage = 1 + let res + const result = [] as string[] + do { + res = await got( + `${this.baseUrl}/repos/${this.username}/${repo}/branches`, + getOptions('GET', this.commonHeaders, { page: initPage, per_page: 100 }, 'json', undefined, undefined, this.proxy) + ) as any + if (res.statusCode === 200) { + res.body.forEach((item: any) => result.push(item.name)) + } else { + return [] + } + initPage++ + } while (res.body.length > 0) + return result + } + + async getBucketListBackstage (configMap: IStringKeyMap): Promise { + const window = windowManager.get(IWindowList.SETTING_WINDOW)! + const { bucketName: repo, customUrl: branch, prefix, cancelToken, cdnUrl } = configMap + const slicedPrefix = prefix.replace(/^\//, '').replace(/\/$/, '') + const cancelTask = [false] + ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { + if (token === cancelToken) { + cancelTask[0] = true + ipcMain.removeAllListeners('cancelLoadingFileList') + } + }) + let res = {} as any + const result = { + fullList: [], + success: false, + finished: false + } + res = await got( + `${this.baseUrl}/repos/${this.username}/${repo}/git/trees/${branch}:${slicedPrefix}`, + getOptions('GET', this.commonHeaders, undefined, 'json', undefined, undefined, this.proxy) + ) + if (res && res.statusCode === 200) { + res.body.tree.forEach((item: any) => { + if (item.type === 'tree') { + result.fullList.push(this.formatFolder(item, slicedPrefix)) + } else { + result.fullList.push(this.formatFile(item, slicedPrefix, branch, repo, cdnUrl)) + } + }) + } else { + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + return + } + result.success = true + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + } + + /** + * 删除文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string + * } + */ + async deleteBucketFile (configMap: IStringKeyMap): Promise { + const { bucketName: repo, githubBranch: branch, key, DeleteHash: sha } = configMap + const body = { + message: 'deleted by PicList', + sha, + branch + } + const res = await got( + `${this.baseUrl}/repos/${this.username}/${repo}/contents/${key}`, + getOptions('DELETE', this.commonHeaders, undefined, 'json', JSON.stringify(body), undefined, this.proxy) + ) + return res.statusCode === 200 + } + + /** + * create a new tree to delete a folder + * @param configMap + */ + async deleteBucketFolder (configMap: IStringKeyMap): Promise { + const { bucketName: repo, githubBranch: branch, key } = configMap + const refRes = await got( + `${this.baseUrl}/repos/${this.username}/${repo}/git/refs/heads/${branch}`, + getOptions('GET', this.commonHeaders, undefined, 'json', undefined, undefined, this.proxy) + ) as any + if (refRes.statusCode !== 200) { + return false + } + const refSha = refRes.body.object.sha + const rootRes = await got( + `${this.baseUrl}/repos/${this.username}/${repo}/branches/${branch}`, + getOptions('GET', undefined, undefined, 'json', undefined, undefined, this.proxy) + ) as any + if (rootRes.statusCode !== 200) { + return false + } + const rootSha = rootRes.body.commit.commit.tree.sha + // TODO: if there are more than 10000 files in the folder, it will be truncated + // Rare cases, not considered for now + const treeRes = await got( + `${this.baseUrl}/repos/${this.username}/${repo}/git/trees/${branch}:${key.replace(/^\//, '').replace(/\/$/, '')}`, + getOptions('GET', this.commonHeaders, { + recursive: true + }, 'json', undefined, undefined, this.proxy) + ) as any + if (treeRes.statusCode !== 200) { + return false + } + const oldTree = treeRes.body.tree + const newTree = oldTree.filter((item: any) => item.type === 'blob') + .map((item:any) => ({ + path: `${key.replace(/^\//, '').replace(/\/$/, '')}/${item.path}`, + mode: item.mode, + type: item.type, + sha: null + })) + const newTreeShaRes = await got( + `${this.baseUrl}/repos/${this.username}/${repo}/git/trees`, + getOptions('POST', this.commonHeaders, undefined, 'json', JSON.stringify({ + base_tree: rootSha, + tree: newTree + }), undefined, this.proxy) + ) as any + if (newTreeShaRes.statusCode !== 201) { + return false + } + const newTreeSha = newTreeShaRes.body.sha + const commitRes = await got( + `${this.baseUrl}/repos/${this.username}/${repo}/git/commits`, + getOptions('POST', this.commonHeaders, undefined, 'json', JSON.stringify({ + message: 'deleted by PicList', + tree: newTreeSha, + parents: [refSha] + }), undefined, this.proxy) + ) as any + if (commitRes.statusCode !== 201) { + return false + } + const commitSha = commitRes.body.sha + const updateRefRes = await got( + `${this.baseUrl}/repos/${this.username}/${repo}/git/refs/heads/${branch}`, + getOptions('PATCH', this.commonHeaders, undefined, 'json', JSON.stringify({ + sha: commitSha + }), undefined, this.proxy) + ) as any + if (updateRefRes.statusCode !== 200) { + return false + } + return true + } + + /** + * 获取预签名url + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string, + * expires: number, + * customUrl: string + * } + */ + async getPreSignedUrl (configMap: IStringKeyMap): Promise { + const { bucketName: repo, customUrl: branch, key, rawUrl, githubPrivate: isPrivate } = configMap + if (!isPrivate) { + return rawUrl + } + const res = await got( + `${this.baseUrl}/repos/${this.username}/${repo}/contents/${key}`, + getOptions('GET', this.commonHeaders, { + ref: branch + }, 'json', undefined, undefined, this.proxy) + ) as any + if (res.statusCode === 200) { + return res.body.download_url + } else { + return '' + } + } + + /** + * 新建文件夹 + * @param configMap + */ + async createBucketFolder (configMap: IStringKeyMap): Promise { + const { bucketName: repo, githubBranch: branch, key } = configMap + const newFileKey = `${trimPath(key)}/.gitkeep` + const base64Content = Buffer.from('created by PicList').toString('base64') + const body = { + message: `created a new folder named ${key} by PicList`, + content: base64Content, + branch + } + const res = await got( + `${this.baseUrl}/repos/${this.username}/${repo}/contents/${newFileKey}`, + getOptions('PUT', this.commonHeaders, undefined, 'json', JSON.stringify(body), undefined, this.proxy) + ) + return res.statusCode === 201 + } + + /** + * 上传文件 + * @param configMap + */ + async uploadBucketFile (configMap: IStringKeyMap): Promise { + const { fileArray } = configMap + const instance = UpDownTaskQueue.getInstance() + fileArray.forEach((item: any) => { + item.key.startsWith('/') && (item.key = item.key.slice(1)) + }) + const filteredFileArray = fileArray.filter((item: any) => item.fileSize < 100 * 1024 * 1024) + for (const item of filteredFileArray) { + const { bucketName: repo, region, githubBranch: branch, key, filePath, fileName } = item + const id = `${repo}-${branch}-${key}-${filePath}` + if (instance.getUploadTask(id)) { + continue + } + const trimKey = trimPath(key) + const base64Content = fs.readFileSync(filePath, { encoding: 'base64' }) + instance.addUploadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + sourceFilePath: filePath, + targetFilePath: key, + targetFileBucket: repo, + targetFileRegion: region + }) + gotUpload( + instance, + `${this.baseUrl}/repos/${this.username}/${repo}/contents/${trimKey}`, + 'PUT', + JSON.stringify({ + message: 'uploaded by PicList', + branch, + content: base64Content + }), + this.commonHeaders, + id, + this.logger, + 30000, + false, + getAgent(this.proxy) + ) + } + return true + } + + /** + * 下载文件 + * @param configMap + */ + async downloadBucketFile (configMap: IStringKeyMap): Promise { + const { downloadPath, fileArray } = configMap + const instance = UpDownTaskQueue.getInstance() + for (const item of fileArray) { + const { bucketName: repo, customUrl: branch, key, fileName, githubPrivate, githubUrl } = item + const id = `${repo}-${branch}-${key}-${fileName}` + const savedFilePath = path.join(downloadPath, fileName) + const fileStream = fs.createWriteStream(savedFilePath) + if (instance.getDownloadTask(id)) { + continue + } + instance.addDownloadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + targetFilePath: savedFilePath + }) + let downloadUrl + if (githubPrivate) { + const preSignedUrl = await this.getPreSignedUrl({ + bucketName: repo, + customUrl: branch, + key, + rawUrl: githubUrl, + githubPrivate + }) + downloadUrl = preSignedUrl + } else { + downloadUrl = githubUrl + } + gotDownload( + instance, + downloadUrl, + fileStream, + id, + savedFilePath, + this.logger, + undefined, + getAgent(this.proxy) + ) + } + return true + } +} + +export default GithubApi diff --git a/src/main/manage/apis/imgur.ts b/src/main/manage/apis/imgur.ts new file mode 100644 index 0000000..1577abb --- /dev/null +++ b/src/main/manage/apis/imgur.ts @@ -0,0 +1,262 @@ +import got from 'got' +import ManageLogger from '../utils/logger' +import { getAgent, getOptions, gotDownload, gotUpload, getFileMimeType } from '../utils/common' +import windowManager from 'apis/app/window/windowManager' +import { IWindowList } from '#/types/enum' +import { ipcMain, IpcMainEvent } from 'electron' +import { isImage } from '~/renderer/manage/utils/common' +import path from 'path' +import UpDownTaskQueue, +{ + commonTaskStatus +} from '../datastore/upDownTaskQueue' +import FormData from 'form-data' +import fs from 'fs-extra' + +class ImgurApi { + userName: string + accessToken: string + proxy: any + logger: ManageLogger + tokenHeaders: any + idHeaders: any + baseUrl = 'https://api.imgur.com/3' + + constructor (userName: string, accessToken: string, proxy: any, logger: ManageLogger) { + this.userName = userName + this.accessToken = accessToken.startsWith('Bearer ') ? accessToken : `Bearer ${accessToken}` + this.proxy = proxy + this.logger = logger + this.tokenHeaders = { + Authorization: this.accessToken + } + } + + formatFile (item: any) { + return { + ...item, + Key: path.basename(item.link), + key: path.basename(item.link), + fileName: `${item.name}${path.extname(item.link)}`, + formatedTime: new Date(item.datetime * 1000).toLocaleString(), + fileSize: item.size, + isDir: false, + checked: false, + match: false, + isImage: isImage(path.basename(item.link)), + url: item.link, + sha: item.deletehash + } + } + + /** + * get repo list + */ + async getBucketList (): Promise { + let initPage = 0 + let res + const result = [] as any[] + do { + res = await got( + `${this.baseUrl}/account/${this.userName}/albums/ids/${initPage}`, + getOptions('GET', this.tokenHeaders, undefined, 'json', undefined, undefined, this.proxy) + ) as any + if (res.statusCode === 200 && res.body.success) { + res.body.data.forEach((item: any) => { + result.push(item) + }) + } else { + return [] + } + initPage++ + } while (res.body.data.length > 0) + const finalResult = [] as any[] + for (let i = 0; i < result.length; i++) { + const item = result[i] + const res = await got( + `${this.baseUrl}/account/${this.userName}/album/${item}`, + getOptions('GET', this.tokenHeaders, undefined, 'json', undefined, undefined, this.proxy) + ) as any + if (res.statusCode === 200 && res.body.success) { + finalResult.push({ + ...res.body.data, + Name: res.body.data.title, + Location: res.body.data.id, + CreationDate: res.body.data.datetime + }) + } else { + return [] + } + } + finalResult.push({ + Name: '全部', + Location: 'unclassified', + CreationDate: new Date().getTime() + }) + return finalResult + } + + async getBucketListBackstage (configMap: IStringKeyMap): Promise { + const window = windowManager.get(IWindowList.SETTING_WINDOW)! + const { bucketConfig: { Location: albumHash }, cancelToken } = configMap + const cancelTask = [false] + ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { + if (token === cancelToken) { + cancelTask[0] = true + ipcMain.removeAllListeners('cancelLoadingFileList') + } + }) + let res = {} as any + const result = { + fullList: [], + success: false, + finished: false + } + if (albumHash !== 'unclassified') { + res = await got( + `${this.baseUrl}/account/${this.userName}/album/${albumHash}`, + getOptions('GET', this.tokenHeaders, undefined, 'json', undefined, undefined, this.proxy) + ) as any + if (res.statusCode === 200 && res.body.success) { + res.body.data.images.forEach((item: any) => { + result.fullList.push(this.formatFile(item)) + }) + } else { + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + return + } + } else { + let initPage = 0 + do { + res = await got( + `${this.baseUrl}/account/${this.userName}/images/${initPage}`, + getOptions('GET', this.tokenHeaders, undefined, 'json', undefined, undefined, this.proxy) + ) as any + if (res.statusCode === 200 && res.body.success) { + res.body.data.forEach((item: any) => { + result.fullList.push(this.formatFile(item)) + }) + } else { + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + return + } + initPage++ + } while (res.body.data.length > 0) + } + result.success = true + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + } + + async deleteBucketFile (configMap: IStringKeyMap): Promise { + const { DeleteHash: deleteHash } = configMap + const res = await got( + `${this.baseUrl}/account/${this.userName}/image/${deleteHash}`, + getOptions('DELETE', this.tokenHeaders, undefined, 'json', undefined, undefined, this.proxy) + ) as any + return res.statusCode === 200 && res.body.success + } + + /** + * 上传文件 + * @param configMap + */ + async uploadBucketFile (configMap: IStringKeyMap): Promise { + const { fileArray } = configMap + const instance = UpDownTaskQueue.getInstance() + fileArray.forEach((item: any) => { + item.key = item.key.replace(/^\/+/, '') + }) + for (const item of fileArray) { + const { bucketName, region: albumHash, key, fileName, filePath, fileSize } = item + const id = `${albumHash}-${key}-${filePath}` + if (instance.getUploadTask(id) || fileSize > 1024 * 1024 * 200) { + continue + } + instance.addUploadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + sourceFilePath: filePath, + targetFilePath: key, + targetFileBucket: bucketName, + targetFileRegion: albumHash + }) + const form = new FormData() + form.append('type', 'file') + form.append('description', 'uploaded by PicList') + form.append('name', path.basename(key, path.extname(key))) + if (fileSize > 1024 * 1024 * 10) { + form.append('video', fs.createReadStream(filePath), { + filename: path.basename(key), + contentType: getFileMimeType(fileName) + }) + } else { + form.append('image', fs.createReadStream(filePath), { + filename: path.basename(key), + contentType: getFileMimeType(fileName) + }) + } + albumHash !== 'unclassified' && form.append('album', albumHash) + const headers = form.getHeaders() + headers.Authorization = this.accessToken + gotUpload( + instance, + `${this.baseUrl}/image`, + 'POST', + form, + headers, + id, + this.logger, + 30000, + false, + getAgent(this.proxy) + ) + } + return true + } + + /** + * 下载文件 + * @param configMap + */ + async downloadBucketFile (configMap: IStringKeyMap): Promise { + const { downloadPath, fileArray } = configMap + const instance = UpDownTaskQueue.getInstance() + for (const item of fileArray) { + const { bucketName, region, key, fileName, githubUrl: url } = item + const id = `${bucketName}-${region}-${key}-${fileName}` + const savedFilePath = path.join(downloadPath, fileName) + const fileStream = fs.createWriteStream(savedFilePath) + if (instance.getDownloadTask(id)) { + continue + } + instance.addDownloadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + targetFilePath: savedFilePath + }) + gotDownload( + instance, + url, + fileStream, + id, + savedFilePath, + this.logger, + undefined, + getAgent(this.proxy) + ) + } + return true + } +} + +export default ImgurApi diff --git a/src/main/manage/apis/qiniu.ts b/src/main/manage/apis/qiniu.ts new file mode 100644 index 0000000..53db286 --- /dev/null +++ b/src/main/manage/apis/qiniu.ts @@ -0,0 +1,655 @@ +import axios from 'axios' +import { hmacSha1Base64, getFileMimeType, gotDownload, formatError } from '../utils/common' +import fs from 'fs-extra' +import qiniu from 'qiniu/index' +import path from 'path' +import { isImage } from '~/renderer/manage/utils/common' +import windowManager from 'apis/app/window/windowManager' +import { IWindowList } from '#/types/enum' +import { ipcMain, IpcMainEvent } from 'electron' +import UpDownTaskQueue, +{ + uploadTaskSpecialStatus, + commonTaskStatus +} from '../datastore/upDownTaskQueue' +import { ManageLogger } from '../utils/logger' + +class QiniuApi { + mac: qiniu.auth.digest.Mac + accessKey: string + secretKey: string + commonType = 'application/x-www-form-urlencoded' + host = 'uc.qiniuapi.com' + logger: ManageLogger + + hostList = { + getBucketList: 'https://uc.qiniuapi.com/buckets', + getBucketDomain: 'https://uc.qiniuapi.com/v2/domains' + } + + constructor (accessKey: string, secretKey: string, logger: ManageLogger) { + this.mac = new qiniu.auth.digest.Mac(accessKey, secretKey) + this.accessKey = accessKey + this.secretKey = secretKey + this.logger = logger + } + + formatFolder (item: string, slicedPrefix: string) { + return { + Key: item, + key: item, + fileSize: 0, + fileName: item.replace(slicedPrefix, '').replace('/', ''), + isDir: true, + checked: false, + isImage: false, + match: false + } + } + + formatFile (item: any, slicedPrefix: string, urlPrefix: string) { + return { + ...item, + fileName: item.key.replace(slicedPrefix, ''), + url: `${urlPrefix}/${item.key}`, + fileSize: item.fsize, + formatedTime: new Date(parseInt(item.putTime.toString().slice(0, -7), 10)).toLocaleString(), + isDir: false, + checked: false, + match: false, + isImage: isImage(item.key.replace(slicedPrefix, '')) + } + } + + authorization ( + method: string, + urlPath: string, + host: string, + body: string, + query: string, + contentType: string, + xQiniuHeaders?: IStringKeyMap + ) { + let signStr = `${method.toUpperCase()} ${urlPath}` + query && (signStr += `?${query}`) + signStr += `\nHost: ${host}` + contentType && (signStr += `\nContent-Type: ${contentType}`) + let xQiniuHeaderStr = '' + if (xQiniuHeaders) { + const xQiniuHeaderKeys = Object.keys(xQiniuHeaders).sort() + xQiniuHeaderKeys.forEach((key) => { + xQiniuHeaderStr += `\n${key}:${xQiniuHeaders[key]}` + }) + signStr += xQiniuHeaderStr + } + signStr += '\n\n' + if (contentType !== 'application/octet-stream' && body) { + signStr += body + } + return `Qiniu ${this.accessKey}:${hmacSha1Base64(this.secretKey, signStr).replace(/\+/g, '-').replace(/\//g, '_')}` + } + + /** + * 获取存储桶列表 + */ + async getBucketList (): Promise { + const host = this.hostList.getBucketList + const authorization = qiniu.util.generateAccessToken(this.mac, host, undefined) + const res = await axios.get(host, { + headers: { + Authorization: authorization, + 'Content-Type': this.commonType + }, + timeout: 10000 + }) + if (res && res.status === 200) { + if (res.data && res.data.length) { + const result = [] as any[] + for (let i = 0; i < res.data.length; i++) { + const info = await this.getBucketInfo({ bucketName: res.data[i] }) + if (!info.success) { + return [] + } + result.push({ + Name: res.data[i], + Location: info.zone, + CreationDate: new Date().toISOString(), + Private: info.private + }) + } + return result + } else { + return [] + } + } else { + return [] + } + } + + /** + * 获取存储桶详细信息 + */ + async getBucketInfo (param: IStringKeyMap): Promise { + const { bucketName } = param + const urlPath = `/v2/bucketInfo?bucket=${bucketName}&fs=true` + const authorization = this.authorization('POST', urlPath, this.host, '', '', 'application/json') + const res = await axios({ + method: 'post', + url: `https://${this.host}/v2/bucketInfo`, + params: { + bucket: bucketName, + fs: true + }, + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + Host: this.host + }, + timeout: 10000 + }) + if (res && res.status === 200) { + return { + success: true, + private: res.data.private, + zone: res.data.zone + } + } else { + return { + success: false + } + } + } + + /** + * 获取自定义域名 + */ + async getBucketDomain (param: IStringKeyMap): Promise { + const { bucketName } = param + const host = this.hostList.getBucketDomain + const authorization = qiniu.util.generateAccessToken(this.mac, `${host}?tbl=${bucketName}`, undefined) + const res = await axios.get(host, { + params: { + tbl: bucketName + }, + headers: { + Authorization: authorization, + 'Content-Type': this.commonType + }, + timeout: 10000 + }) + if (res && res.status === 200) { + return res.data && res.data.length ? res.data : [] + } else { + return [] + } + } + + /** + * 修改存储桶权限 + */ + async setBucketAclPolicy (param: IStringKeyMap): Promise { + // 0: 公开访问 1: 私有访问 + const { bucketName } = param + let { isPrivate } = param + isPrivate = isPrivate ? 1 : 0 + const urlPath = `/private?bucket=${bucketName}&private=${isPrivate}` + const authorization = this.authorization('POST', urlPath, this.host, '', '', this.commonType) + const res = await axios({ + method: 'post', + url: `https://${this.host}/private`, + params: { + bucket: bucketName, + private: isPrivate + }, + headers: { + Authorization: authorization, + 'Content-Type': this.commonType, + Host: this.host + }, + timeout: 10000 + }) + return res && res.status === 200 + } + + /** + * 创建存储桶 + * @param {Object} configMap + * configMap = { + * BucketName: string, + * region: string, + * acl: boolean // 是否公开访问 + * } + */ + async createBucket (configMap: IStringKeyMap): Promise { + const { BucketName, region } = configMap + const { acl } = configMap + const urlPath = `/mkbucketv3/${BucketName}/region/${region}` + const authorization = this.authorization('POST', urlPath, this.host, '', '', 'application/json') + const res = await axios({ + method: 'post', + url: `https://${this.host}${urlPath}`, + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + Host: this.host + }, + timeout: 10000 + }) + if (res && res.status === 200) { + const changeAclRes = await this.setBucketAclPolicy({ + bucketName: BucketName, + isPrivate: !acl + }) + return changeAclRes + } else { + return false + } + } + + async getBucketListBackstage (configMap: IStringKeyMap): Promise { + const window = windowManager.get(IWindowList.SETTING_WINDOW)! + const { bucketName: bucket, prefix, cancelToken, customUrl: urlPrefix } = configMap + let marker = undefined as any + const slicedPrefix = prefix.slice(1) + const cancelTask = [false] + ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { + if (token === cancelToken) { + cancelTask[0] = true + ipcMain.removeAllListeners('cancelLoadingFileList') + } + }) + let res = {} as any + const result = { + fullList: [], + success: false, + finished: false + } + const config = new qiniu.conf.Config() + const bucketManager = new qiniu.rs.BucketManager(this.mac, config) + do { + res = await new Promise((resolve, reject) => { + bucketManager.listPrefix(bucket, { + prefix: slicedPrefix === '' ? undefined : slicedPrefix, + delimiter: '/', + marker, + limit: 1000 + }, (err: any, respBody: any, respInfo: any) => { + if (err) { + reject(err) + } else { + resolve({ + respBody, + respInfo + }) + } + }) + }) + if (res && res.respInfo.statusCode === 200) { + res.respBody && res.respBody.commonPrefixes && res.respBody.commonPrefixes.forEach((item: any) => { + result.fullList.push(this.formatFolder(item, slicedPrefix)) + }) + res.respBody && res.respBody.items && res.respBody.items.forEach((item: any) => { + item.fsize !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) + }) + window.webContents.send('refreshFileTransferList', result) + } else { + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + return + } + marker = res.respBody.marker + } while (res.respBody && res.respBody.marker && !cancelTask[0]) + result.success = true + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + } + + /** + * 获取文件列表 + * @param {Object} configMap + * configMap = { + * bucketName: string, + * bucketConfig: { + * Location: string + * }, + * paging: boolean, + * prefix: string, + * marker: string, + * itemsPerPage: number, + * customUrl: string + * } + */ + async getBucketFileList (configMap: IStringKeyMap): Promise { + const { bucketName: bucket, prefix, marker, itemsPerPage, customUrl: urlPrefix } = configMap + const slicedPrefix = prefix.slice(1) + const config = new qiniu.conf.Config() + const bucketManager = new qiniu.rs.BucketManager(this.mac, config) + let res = {} as any + const result = { + fullList: [], + isTruncated: false, + nextMarker: '', + success: false + } + res = await new Promise((resolve, reject) => { + bucketManager.listPrefix(bucket, { + limit: itemsPerPage, + prefix: slicedPrefix === '' ? undefined : slicedPrefix, + marker, + delimiter: '/' + }, (err, respBody, respInfo) => { + if (err) { + reject(err) + } else { + resolve({ + respBody, + respInfo + }) + } + }) + }) + if (res && res.respInfo.statusCode === 200) { + if (res.respBody && res.respBody.commonPrefixes) { + res.respBody.commonPrefixes.forEach((item: string) => { + result.fullList.push(this.formatFolder(item, slicedPrefix)) + }) + } + if (res.respBody && res.respBody.items) { + res.respBody.items.forEach((item: any) => { + item.fsize !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) + }) + } + result.isTruncated = !!(res.respBody && res.respBody.marker) + result.nextMarker = res.respBody && res.respBody.marker ? res.respBody.marker : '' + result.success = true + return result + } else { + return result + } + } + + /** + * 删除文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string + * } + */ + async deleteBucketFile (configMap: IStringKeyMap): Promise { + const { bucketName, key } = configMap + const config = new qiniu.conf.Config() + const bucketManager = new qiniu.rs.BucketManager(this.mac, config) + const res = await new Promise((resolve, reject) => { + bucketManager.delete(bucketName, key, (err, respBody, respInfo) => { + if (err) { + reject(err) + } else { + resolve({ + respBody, + respInfo + }) + } + }) + }) as any + if (res && res.respInfo.statusCode === 200) { + return true + } else { + return false + } + } + + /** + * 删除文件夹 + * @param configMap + */ + async deleteBucketFolder (configMap: IStringKeyMap): Promise { + const { bucketName, key } = configMap + const config = new qiniu.conf.Config() + const bucketManager = new qiniu.rs.BucketManager(this.mac, config) + let marker = '' + let isTruncated = true + const allFileList = { + Contents: [] as any[] + } + do { + const res = await new Promise((resolve, reject) => { + bucketManager.listPrefix(bucketName, { + prefix: key, + marker, + limit: 1000 + }, (err, respBody, respInfo) => { + if (err) { + reject(err) + } else { + resolve({ + respBody, + respInfo + }) + } + }) + }) as any + if (res && res.respInfo.statusCode === 200) { + if (res.respBody && res.respBody.items) { + allFileList.Contents = allFileList.Contents.concat(res.respBody.items) + } + isTruncated = !!(res.respBody && res.respBody.marker) + marker = res.respBody && res.respBody.marker ? res.respBody.marker : '' + } else { + return false + } + } while (isTruncated) + const cycleNum = Math.ceil(allFileList.Contents.length / 1000) + for (let i = 0; i < cycleNum; i++) { + const deleteOps = allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => { + return qiniu.rs.deleteOp(bucketName, item.key) + }) + const res = await new Promise((resolve, reject) => { + bucketManager.batch(deleteOps, (err, respBody, respInfo) => { + if (err) { + reject(err) + } else { + resolve({ + respBody, + respInfo + }) + } + }) + }) as any + if (!(res && res.respInfo.statusCode === 200)) { + return false + } + } + return true + } + + /** + * 重命名文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * oldKey: string, + * newKey: string + * } + */ + async renameBucketFile (configMap: IStringKeyMap): Promise { + const { bucketName, oldKey, newKey } = configMap + const config = new qiniu.conf.Config() + const bucketManager = new qiniu.rs.BucketManager(this.mac, config) + const res = await new Promise((resolve, reject) => { + bucketManager.move(bucketName, oldKey, bucketName, newKey, { + force: true + }, (err, respBody, respInfo) => { + if (err) { + reject(err) + } else { + resolve({ + respBody, + respInfo + }) + } + }) + }) as any + return res && res.respInfo.statusCode === 200 + } + + /** + * 获取预签名url + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string, + * expires: number, + * customUrl: string + * } + */ + async getPreSignedUrl (configMap: IStringKeyMap): Promise { + const { key, expires, customUrl } = configMap + const config = new qiniu.conf.Config() + const bucketManager = new qiniu.rs.BucketManager(this.mac, config) + const urlPrefix = customUrl + const expiration = parseInt(Date.now() / 1000 + expires) + const res = bucketManager.privateDownloadUrl(urlPrefix, key, expiration) + return res + } + + /** + * 上传文件 + * @param configMap + */ + async uploadBucketFile (configMap: IStringKeyMap): Promise { + const { fileArray } = configMap + const instance = UpDownTaskQueue.getInstance() + fileArray.forEach((item: any) => { + item.key = item.key.replace(/^\/+/, '') + }) + for (const item of fileArray) { + const { bucketName, region, key, filePath, fileName } = item + instance.addUploadTask({ + id: `${bucketName}-${region}-${key}-${filePath}`, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + sourceFilePath: filePath, + targetFilePath: key, + targetFileBucket: bucketName, + targetFileRegion: region + }) + const config = new qiniu.conf.Config() + const resumeUploader = new qiniu.resume_up.ResumeUploader(config) + const putExtra = new qiniu.resume_up.PutExtra() + const uploadToken = new qiniu.rs.PutPolicy({ + scope: `${bucketName}:${key}`, + expires: 36000 + }).uploadToken(this.mac) + putExtra.fname = key + putExtra.params = {} + putExtra.mimeType = getFileMimeType(fileName) + putExtra.version = 'v2' + putExtra.partSize = 4 * 1024 * 1024 + putExtra.progressCallback = (uploadBytes, totalBytes) => { + const progress = Math.floor(uploadBytes / totalBytes * 100) + instance.updateUploadTask({ + id: `${bucketName}-${region}-${key}-${filePath}`, + progress, + status: uploadTaskSpecialStatus.uploading + }) + } + resumeUploader.putFile(uploadToken, key, filePath, putExtra, (respErr, respBody, respInfo) => { + if (respErr) { + this.logger.error(formatError(respErr, { class: 'Qiniu', method: 'uploadBucketFile' })) + instance.updateUploadTask({ + id: `${bucketName}-${region}-${key}-${filePath}`, + progress: 0, + status: commonTaskStatus.failed, + finishTime: new Date().toLocaleString() + }) + return + } + if (respInfo.statusCode === 200) { + instance.updateUploadTask({ + id: `${bucketName}-${region}-${key}-${filePath}`, + progress: 100, + status: uploadTaskSpecialStatus.uploaded, + response: JSON.stringify(respBody), + finishTime: new Date().toLocaleString() + }) + } else { + instance.updateUploadTask({ + id: `${bucketName}-${region}-${key}-${filePath}`, + progress: 0, + status: commonTaskStatus.failed, + finishTime: new Date().toLocaleString() + }) + } + }) + } + return true + } + + /** + * 新建文件夹 + * @param configMap + */ + async createBucketFolder (configMap: IStringKeyMap): Promise { + const { bucketName, key } = configMap + const putPolicy = new qiniu.rs.PutPolicy({ + scope: `${bucketName}:${key}` + }) + const uploadToken = putPolicy.uploadToken(this.mac) + const FormUploader = new qiniu.form_up.FormUploader() + const putExtra = new qiniu.form_up.PutExtra() + const res = await new Promise((resolve, reject) => { + FormUploader.put(uploadToken, key, '', putExtra, (err, respBody, respInfo) => { + if (err) { + reject(err) + } else { + resolve({ + respBody, + respInfo + }) + } + }) + }) as any + if (res && res.respInfo.statusCode === 200) { + return true + } else { + return false + } + } + + /** + * 下载文件 + * @param configMap + */ + async downloadBucketFile (configMap: IStringKeyMap): Promise { + const { downloadPath, fileArray } = configMap + const instance = UpDownTaskQueue.getInstance() + for (const item of fileArray) { + const { bucketName, region, key, fileName, customUrl } = item + const savedFilePath = path.join(downloadPath, fileName) + const fileStream = fs.createWriteStream(savedFilePath) + const id = `${bucketName}-${region}-${key}` + if (instance.getDownloadTask(id)) { + continue + } + instance.addDownloadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + targetFilePath: savedFilePath + }) + const preSignedUrl = await this.getPreSignedUrl({ key, expires: 36000, customUrl }) + gotDownload(instance, preSignedUrl, fileStream, id, savedFilePath, this.logger) + } + return true + } +} + +export default QiniuApi diff --git a/src/main/manage/apis/smms.ts b/src/main/manage/apis/smms.ts new file mode 100644 index 0000000..8696bec --- /dev/null +++ b/src/main/manage/apis/smms.ts @@ -0,0 +1,248 @@ +import { isImage } from '@/manage/utils/common' +import axios, { AxiosInstance } from 'axios' +import windowManager from 'apis/app/window/windowManager' +import { IWindowList } from '#/types/enum' +import { ipcMain, IpcMainEvent } from 'electron' +import FormData from 'form-data' +import fs from 'fs-extra' +import { getFileMimeType, gotUpload, gotDownload } from '../utils/common' +import path from 'path' +import UpDownTaskQueue, { commonTaskStatus } from '../datastore/upDownTaskQueue' +import { ManageLogger } from '../utils/logger' + +class SmmsApi { + baseUrl = 'https://smms.app/api/v2' + token: string + axiosInstance: AxiosInstance + logger: ManageLogger + + constructor (token: string, logger: ManageLogger) { + this.token = token + this.axiosInstance = axios.create({ + baseURL: this.baseUrl, + timeout: 30000, + headers: { + Authorization: this.token + } + }) + this.logger = logger + } + + formatFile (item: any) { + return { + ...item, + Key: item.path, + key: item.path, + fileName: item.filename, + fileSize: item.size, + formatedTime: new Date(item.created_at).toLocaleString(), + isDir: false, + checked: false, + match: false, + isImage: isImage(item.storename), + sha: item.hash, + downloadUrl: item.url + } + } + + async getBucketListBackstage (configMap: IStringKeyMap): Promise { + const window = windowManager.get(IWindowList.SETTING_WINDOW)! + const { cancelToken } = configMap + let marker = 1 + const cancelTask = [false] + ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { + if (token === cancelToken) { + cancelTask[0] = true + ipcMain.removeAllListeners('cancelLoadingFileList') + } + }) + let res = {} as any + const result = { + fullList: [], + success: false, + finished: false + } + do { + res = await this.axiosInstance( + '/upload_history', + { + method: 'GET', + headers: { + 'Content-Type': 'multipart/form-data' + }, + params: { + page: marker + } + }) + if (res && res.status === 200 && res.data && res.data.success) { + if (res.data.Count === 0) { + result.success = true + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + return + } else { + res.data.data.forEach((item: any) => { + result.fullList.push(this.formatFile(item)) + }) + window.webContents.send('refreshFileTransferList', result) + } + } else { + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + return + } + marker++ + } while (!cancelTask[0] && res && res.status === 200 && res.data && res.data.success && res.data.CurrentPage < res.data.TotalPages) + result.success = true + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + } + + /** + * 获取文件列表 + * @param {Object} configMap + * configMap = { + * bucketName: string, + * bucketConfig: { + * Location: string + * }, + * paging: boolean, + * prefix: string, + * marker: string, + * itemsPerPage: number, + * customUrl: string + * } + */ + async getBucketFileList (configMap: IStringKeyMap): Promise { + const { currentPage } = configMap + let res = {} as any + const result = { + fullList: [], + isTruncated: false, + nextMarker: '', + success: false + } + res = await this.axiosInstance( + '/upload_history', + { + method: 'GET', + headers: { + 'Content-Type': 'multipart/form-data' + }, + params: { + page: currentPage + } + } + ) + if (res && res.status === 200 && res.data && res.data.success) { + if (res.data.Count === 0) { + result.success = true + return result + } + res.data.data.forEach((item: any) => { + result.fullList.push(this.formatFile(item)) + }) + result.isTruncated = res.data.CurrentPage < res.data.TotalPages + result.nextMarker = res.data.CurrentPage + 1 + result.success = true + return result + } else { + return result + } + } + + /** + * 删除文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string, + * DeleteHash: string + * } + */ + async deleteBucketFile (configMap: IStringKeyMap): Promise { + const { DeleteHash } = configMap + const params = { + hash: DeleteHash, + format: 'json' + } + const res = await this.axiosInstance( + `/delete/${DeleteHash}`, + { + method: 'GET', + params + } + ) + return res && res.status === 200 && res.data && res.data.success + } + + /** + * 上传文件 + * @param configMap + */ + async uploadBucketFile (configMap: IStringKeyMap): Promise { + const { fileArray } = configMap + const instance = UpDownTaskQueue.getInstance() + for (const item of fileArray) { + const { bucketName, region, key, filePath, fileName } = item + const id = `${bucketName}-${region}-${key}-${filePath}` + if (instance.getUploadTask(id)) { + continue + } + instance.addUploadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + sourceFilePath: filePath, + targetFilePath: key, + targetFileBucket: bucketName, + targetFileRegion: region + }) + const form = new FormData() + form.append('format', 'json') + form.append('smfile', fs.createReadStream(filePath), { + filename: path.basename(fileName), + contentType: getFileMimeType(fileName) + }) + const headers = form.getHeaders() + headers.Authorization = this.token + const url = `${this.baseUrl}/upload` + gotUpload(instance, url, 'POST', form, headers, id, this.logger) + } + return true + } + + /** + * 下载文件 + * @param configMap + */ + async downloadBucketFile (configMap: IStringKeyMap): Promise { + const { downloadPath, fileArray } = configMap + const instance = UpDownTaskQueue.getInstance() + for (const item of fileArray) { + const { bucketName, region, key, fileName, downloadUrl: preSignedUrl } = item + const savedFilePath = path.join(downloadPath, fileName) + const fileStream = fs.createWriteStream(savedFilePath) + const id = `${bucketName}-${region}-${key}` + if (instance.getDownloadTask(id)) { + continue + } + instance.addDownloadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + targetFilePath: savedFilePath + }) + gotDownload(instance, preSignedUrl, fileStream, id, savedFilePath, this.logger) + } + return true + } +} + +export default SmmsApi diff --git a/src/main/manage/apis/tcyun.ts b/src/main/manage/apis/tcyun.ts new file mode 100644 index 0000000..4ec8c00 --- /dev/null +++ b/src/main/manage/apis/tcyun.ts @@ -0,0 +1,523 @@ +import COS from 'cos-nodejs-sdk-v5' +import fs from 'fs-extra' +import path from 'path' +import { isImage } from '~/renderer/manage/utils/common' +import { handleUrlEncode } from '~/universal/utils/common' +import windowManager from 'apis/app/window/windowManager' +import { IWindowList } from '#/types/enum' +import { ipcMain, IpcMainEvent } from 'electron' +import { formatError, getFileMimeType } from '../utils/common' +import UpDownTaskQueue, +{ + uploadTaskSpecialStatus, + commonTaskStatus, + downloadTaskSpecialStatus +} from '../datastore/upDownTaskQueue' +import { ManageLogger } from '../utils/logger' + +class TcyunApi { + ctx: COS + logger: ManageLogger + + constructor (secretId: string, secretKey: string, logger: ManageLogger) { + this.ctx = new COS({ + SecretId: secretId, + SecretKey: secretKey + }) + this.logger = logger + } + + formatFolder (item: {Prefix: string}, slicedPrefix: string): any { + return { + ...item, + key: item.Prefix, + fileSize: 0, + formatedTime: '', + fileName: item.Prefix.replace(slicedPrefix, '').replace('/', ''), + isDir: true, + checked: false, + isImage: false, + match: false + } + } + + formatFile (item: COS.CosObject, slicedPrefix: string, urlPrefix: string): any { + return { + ...item, + key: item.Key, + fileName: item.Key.replace(slicedPrefix, ''), + fileSize: parseInt(item.Size), + formatedTime: new Date(item.LastModified).toLocaleString(), + isDir: false, + checked: false, + isImage: isImage(item.Key), + match: false, + url: `${urlPrefix}/${item.Key}` + } + } + + /** + * 获取存储桶列表 + */ + async getBucketList (): Promise { + const res = await this.ctx.getService({}) + return res && res.Buckets ? res.Buckets : [] + } + + /** + * 获取自定义域名 + */ + async getBucketDomain (param: IStringKeyMap): Promise { + const { bucketName, region } = param + const res = await this.ctx.getBucketDomain({ + Bucket: bucketName, + Region: region + }) + const result = [] as string[] + if (res && res.statusCode === 200) { + if (res.DomainRule && res.DomainRule.length > 0) { + res.DomainRule.forEach((item: any) => { + if (item.Status === 'ENABLED') { + result.push(item.Name) + } + }) + return result + } else { + return [] + } + } else { + return [] + } + } + + /** + * 创建存储桶 + * @param {Object} configMap + * configMap = { + * BucketName: string, + * region: string, + * acl: string + * } + * @description + * acl: private | publicRead | publicReadWrite + */ + async createBucket (configMap: IStringKeyMap): Promise < boolean > { + const aclTransMap: IStringKeyMap = { + private: 'private', + publicRead: 'public-read', + publicReadWrite: 'public-read-write' + } + const res = await this.ctx.putBucket({ + ACL: aclTransMap[configMap.acl], + Bucket: configMap.BucketName, + Region: configMap.region + }) + return res && res.statusCode === 200 + } + + async getBucketListBackstage (configMap: IStringKeyMap): Promise < any > { + const window = windowManager.get(IWindowList.SETTING_WINDOW)! + const bucket = configMap.bucketName + const region = configMap.bucketConfig.Location + const prefix = configMap.prefix as string + const slicedPrefix = prefix.slice(1, prefix.length) + const urlPrefix = configMap.customUrl || `https://${bucket}.cos.${region}.myqcloud.com` + let marker + const cancelToken = configMap.cancelToken as string + const cancelTask = [false] + ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { + if (token === cancelToken) { + cancelTask[0] = true + ipcMain.removeAllListeners('cancelLoadingFileList') + } + }) + let res = {} as COS.GetBucketResult + const result = { + fullList: [], + success: false, + finished: false + } + do { + res = await this.ctx.getBucket({ + Bucket: bucket, + Region: region, + Prefix: slicedPrefix === '' ? undefined : slicedPrefix, + Delimiter: '/', + Marker: marker + }) + if (res && res.statusCode === 200) { + res.CommonPrefixes.forEach((item: { Prefix: string}) => + result.fullList.push(this.formatFolder(item, slicedPrefix))) + res.Contents.forEach((item: COS.CosObject) => + parseInt(item.Size) !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))) + window.webContents.send('refreshFileTransferList', result) + } else { + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + return + } + marker = res.NextMarker + } while (res.IsTruncated === 'true' && !cancelTask[0]) + result.success = true + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + } + + /** + * 获取文件列表 + * @param {Object} configMap + * configMap = { + * bucketName: string, + * bucketConfig: { + * Location: string + * }, + * paging: boolean, + * prefix: string, + * marker: string, + * itemsPerPage: number, + * customUrl: string + * } + */ + async getBucketFileList (configMap: IStringKeyMap): Promise { + const bucket = configMap.bucketName + const region = configMap.bucketConfig.Location + const prefix = configMap.prefix as string + const slicedPrefix = prefix.slice(1) + const urlPrefix = configMap.customUrl || `https://${bucket}.cos.${region}.myqcloud.com` + const marker = configMap.marker as string + const itemsPerPage = configMap.itemsPerPage as number + let res = {} as COS.GetBucketResult + const result = { + fullList: [], + isTruncated: false, + nextMarker: '', + success: false + } + res = await this.ctx.getBucket({ + Bucket: bucket, + Region: region, + Prefix: slicedPrefix === '' ? undefined : slicedPrefix, + Delimiter: '/', + Marker: marker, + MaxKeys: itemsPerPage + }) + if (res && res.statusCode === 200) { + res.CommonPrefixes.forEach((item: { Prefix: string}) => + result.fullList.push(this.formatFolder(item, slicedPrefix))) + res.Contents.forEach((item: COS.CosObject) => + parseInt(item.Size) !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))) + result.isTruncated = res.IsTruncated === 'true' + result.nextMarker = res.NextMarker || '' + result.success = true + return result + } else { + return result + } + } + + /** + * 重命名文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * oldKey: string, + * newKey: string + * } + */ + async renameBucketFile (configMap: IStringKeyMap): Promise { + const { bucketName, region, oldKey, newKey } = configMap + const res = await this.ctx.putObjectCopy({ + Bucket: bucketName, + Region: region, + Key: newKey, + CopySource: handleUrlEncode(`${bucketName}.cos.${region}.myqcloud.com/${oldKey}`) + }) + if (res && res.statusCode === 200) { + const res2 = await this.ctx.deleteObject({ + Bucket: bucketName, + Region: region, + Key: oldKey + }) + return res2 && res2.statusCode === 204 + } else { + return false + } + } + + /** + * 删除文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string + * } + */ + async deleteBucketFile (configMap: IStringKeyMap): Promise { + const { bucketName, region, key } = configMap + const res = await this.ctx.deleteObject({ + Bucket: bucketName, + Region: region, + Key: key + }) + return res && res.statusCode === 204 + } + + /** + * 删除文件夹 + * @param configMap + */ + async deleteBucketFolder (configMap: IStringKeyMap): Promise { + const { bucketName, region, key } = configMap + let marker + let isTruncated + const allFileList = { + CommonPrefixes: [] as any[], + Contents: [] as any[] + } + let res = await this.ctx.getBucket({ + Bucket: bucketName, + Region: region, + Prefix: key, + Delimiter: '/', + MaxKeys: 1000 + }) + if (res && res.statusCode === 200) { + res.CommonPrefixes.length > 0 && allFileList.CommonPrefixes.push(...res.CommonPrefixes) + res.Contents.length > 0 && allFileList.Contents.push(...res.Contents) + isTruncated = res.IsTruncated + marker = res.NextMarker + while (isTruncated === 'true') { + res = await this.ctx.getBucket({ + Bucket: bucketName, + Region: region, + Prefix: key, + Delimiter: '/', + Marker: marker, + MaxKeys: 1000 + }) as any + if (res && res.statusCode === 200) { + res.CommonPrefixes.length > 0 && allFileList.CommonPrefixes.push(...res.CommonPrefixes) + res.Contents.length > 0 && allFileList.Contents.push(...res.Contents) + isTruncated = res.IsTruncated + marker = res.NextMarker + } else { + return false + } + } + } else { + return false + } + if (allFileList.CommonPrefixes.length > 0) { + for (const item of allFileList.CommonPrefixes) { + res = await this.deleteBucketFolder({ + bucketName, + region, + key: item.Prefix + }) as any + if (!res) { + return false + } + } + } + if (allFileList.Contents.length > 0) { + const cycle = Math.ceil(allFileList.Contents.length / 1000) + for (let i = 0; i < cycle; i++) { + res = await this.ctx.deleteMultipleObject({ + Bucket: bucketName, + Region: region, + Objects: allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => { + return { + Key: item.Key + } + }) + }) as any + if (!(res && res.statusCode === 200)) { + return false + } + } + } + return true + } + + /** + * 获取预签名url + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string, + * expires: number, + * customUrl: string + * } + */ + async getPreSignedUrl (configMap: IStringKeyMap): Promise { + const { bucketName, region, key, expires, customUrl } = configMap + const res = this.ctx.getObjectUrl({ + Bucket: bucketName, + Region: region, + Key: key, + Expires: expires, + Sign: true + }, () => { + }) + return customUrl ? `${customUrl.replace(/\/$/, '')}/${key}${res.slice(res.indexOf('?'))}` : res + } + + /** + * 高级上传文件 + * @param configMap + */ + async uploadBucketFile (configMap: IStringKeyMap): Promise { + const { fileArray } = configMap + // fileArray = [{ + // bucketName: string, + // region: string, + // key: string, + // filePath: string + // fileSize: number + // }] + const instance = UpDownTaskQueue.getInstance() + const files = [] as any[] + for (const item of fileArray) { + const { bucketName, region, key, filePath, fileSize, fileName } = item + const id = `${bucketName}-${region}-${key}-${filePath}` + if (instance.getUploadTask(id)) { + continue + } + instance.addUploadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + sourceFilePath: filePath, + targetFilePath: key, + targetFileBucket: bucketName, + targetFileRegion: region + }) + files.push({ + Bucket: bucketName, + Region: region, + Key: key, + FilePath: filePath, + ContentType: getFileMimeType(filePath), + Body: fileSize > 1048576 ? fs.createReadStream(filePath) : undefined, + onProgress: (progress: any) => { + const cancelToken = '' + instance.updateUploadTask({ + id, + progress: Math.floor(progress.percent * 100), + status: uploadTaskSpecialStatus.uploading, + cancelToken + }) + }, + onFileFinish: (err: any, data: any) => { + if (data) { + instance.updateUploadTask({ + id, + progress: 100, + status: uploadTaskSpecialStatus.uploaded, + response: typeof data === 'object' ? JSON.stringify(data) : String(data), + finishTime: new Date().toLocaleString() + }) + } else { + this.logger.error(formatError(err, { method: 'uploadBucketFile', class: 'TcyunApi' })) + instance.updateUploadTask({ + id, + progress: 0, + status: commonTaskStatus.failed, + response: typeof err === 'object' ? JSON.stringify(err) : String(err), + finishTime: new Date().toLocaleString() + }) + } + } + }) + this.ctx.uploadFiles({ + files + }) + } + return true + } + + /** + * 新建文件夹 + * @param configMap + */ + async createBucketFolder (configMap: IStringKeyMap): Promise { + const { bucketName, region, key } = configMap + const res = await this.ctx.putObject({ + Bucket: bucketName, + Region: region, + Key: key, + Body: '' + }) + return res && res.statusCode === 200 + } + + /** + * 下载文件 + * @param configMap + */ + async downloadBucketFile (configMap: IStringKeyMap): Promise { + const { downloadPath, fileArray } = configMap + // fileArray = [{ + // bucketName: string, + // region: string, + // key: string, + // fileName: string + // }] + const instance = UpDownTaskQueue.getInstance() + for (const item of fileArray) { + const { bucketName, region, key, fileName } = item + const id = `${bucketName}-${region}-${key}` + if (instance.getDownloadTask(id)) { + continue + } + instance.addDownloadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + targetFilePath: path.join(downloadPath, fileName) + }) + this.ctx.downloadFile({ + Bucket: bucketName, + Region: region, + Key: key, + RetryTimes: 3, + ChunkSize: 1024 * 1024 * 1, + FilePath: path.join(downloadPath, fileName), + onProgress: (progress: any) => { + instance.updateDownloadTask({ + id, + progress: Math.floor(progress.percent * 100), + status: downloadTaskSpecialStatus.downloading + }) + } + }).then((res: any) => { + instance.updateDownloadTask({ + id, + progress: res && res.statusCode === 200 ? 100 : 0, + status: res && res.statusCode === 200 ? downloadTaskSpecialStatus.downloaded : commonTaskStatus.failed, + response: typeof res === 'object' ? JSON.stringify(res) : String(res), + finishTime: new Date().toLocaleString() + }) + }).catch((err: any) => { + this.logger.error(formatError(err, { method: 'downloadBucketFile', class: 'TcyunApi' })) + instance.updateDownloadTask({ + id, + progress: 0, + status: commonTaskStatus.failed, + response: typeof err === 'object' ? JSON.stringify(err) : String(err), + finishTime: new Date().toLocaleString() + }) + }) + } + return true + } +} + +export default TcyunApi diff --git a/src/main/manage/apis/upyun.ts b/src/main/manage/apis/upyun.ts new file mode 100644 index 0000000..9f0f594 --- /dev/null +++ b/src/main/manage/apis/upyun.ts @@ -0,0 +1,388 @@ +// @ts-ignore +import Upyun from 'upyun' +import { md5, hmacSha1Base64, getFileMimeType, gotDownload, gotUpload } from '../utils/common' +import { isImage } from '~/renderer/manage/utils/common' +import windowManager from 'apis/app/window/windowManager' +import { IWindowList } from '#/types/enum' +import { ipcMain, IpcMainEvent } from 'electron' +import axios from 'axios' +import FormData from 'form-data' +import fs from 'fs-extra' +import path from 'path' +import UpDownTaskQueue, +{ + commonTaskStatus +} from '../datastore/upDownTaskQueue' +import { ManageLogger } from '../utils/logger' + +class UpyunApi { + ser: Upyun.Service + cli: Upyun.Client + bucket: string + operator: string + password: string + stopMarker = 'g2gCZAAEbmV4dGQAA2VvZg' + logger: ManageLogger + + constructor (bucket: string, operator: string, password: string, logger: ManageLogger) { + this.ser = new Upyun.Service(bucket, operator, password) + this.cli = new Upyun.Client(this.ser) + this.bucket = bucket + this.operator = operator + this.password = password + this.logger = logger + } + + formatFolder (item: any, slicedPrefix: string) { + return { + ...item, + key: `${slicedPrefix}${item.name}/`, + fileSize: 0, + formatedTime: '', + fileName: item.name, + isDir: true, + checked: false, + isImage: false, + match: false, + Key: `${slicedPrefix}${item.name}/` + } + } + + formatFile (item: any, slicedPrefix: string, urlPrefix: string) { + return { + ...item, + fileName: item.name, + fileSize: item.size, + formatedTime: new Date(parseInt(item.time) * 1000).toLocaleString(), + isDir: false, + checked: false, + match: false, + isImage: isImage(item.name), + url: `${urlPrefix}/${slicedPrefix}${item.name}`, + key: `${slicedPrefix}${item.name}` + } + } + + authorization ( + method: string, + uri: string, + contentMd5: string, + operator: string, + password: string + ) { + const passwordMd5 = md5(password, 'hex') + const date = new Date().toUTCString() + const upperMethod = method.toUpperCase() + let stringToSign = '' + const codedUri = encodeURI(uri) + if (contentMd5 === '') { + stringToSign = `${upperMethod}&${codedUri}&${date}` + } else { + stringToSign = `${upperMethod}&${codedUri}&${date}&${contentMd5}` + } + const signature = hmacSha1Base64(passwordMd5, stringToSign) + return `UPYUN ${operator}:${signature}` + } + + /** + * 获取空间列表 + */ + async getBucketList (): Promise { + return this.bucket + } + + async getBucketListBackstage (configMap: IStringKeyMap): Promise { + const window = windowManager.get(IWindowList.SETTING_WINDOW)! + const { bucketName: bucket, prefix, cancelToken } = configMap + const slicedPrefix = prefix.slice(1) + const urlPrefix = configMap.customUrl || `http://${bucket}.test.upcdn.net` + let marker = '' + const cancelTask = [false] + ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { + if (token === cancelToken) { + cancelTask[0] = true + ipcMain.removeAllListeners('cancelLoadingFileList') + } + }) + let res = {} as any + const result = { + fullList: [], + success: false, + finished: false + } + do { + res = await this.cli.listDir(prefix, { + limit: 10000, + iter: marker + }) + if (res) { + res.files && res.files.forEach((item: any) => { + item.type === 'N' && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) + item.type === 'F' && result.fullList.push(this.formatFolder(item, slicedPrefix)) + }) + window.webContents.send('refreshFileTransferList', result) + } else { + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + return + } + marker = res.next + } while (!cancelTask[0] && res.next !== this.stopMarker) + result.success = true + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + } + + /** + * 获取文件列表 + * @param {Object} configMap + * configMap = { + * bucketName: string, + * bucketConfig: { + * Location: string + * }, + * paging: boolean, + * prefix: string, + * marker: string, + * itemsPerPage: number, + * customUrl: string + * } + */ + async getBucketFileList (configMap: IStringKeyMap): Promise { + const { bucketName: bucket, prefix, marker, itemsPerPage } = configMap + const slicedPrefix = prefix.slice(1) + const urlPrefix = configMap.customUrl || `http://${bucket}.test.upcdn.net` + let res = {} as any + const result = { + fullList: [], + isTruncated: false, + nextMarker: '', + success: false + } + res = await this.cli.listDir(prefix, { + limit: itemsPerPage, + iter: marker || '' + }) + if (res) { + res.files && res.files.forEach((item: any) => { + item.type === 'N' && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) + item.type === 'F' && result.fullList.push(this.formatFolder(item, slicedPrefix)) + }) + result.isTruncated = res.next !== this.stopMarker + result.nextMarker = res.next + result.success = true + return result + } else { + return result + } + } + + /** + * 重命名文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * oldKey: string, + * newKey: string + * } + */ + async renameBucketFile (configMap: IStringKeyMap): Promise { + const oldKey = configMap.oldKey + let newKey = configMap.newKey + const method = 'PUT' + if (newKey.endsWith('/')) { + newKey = newKey.slice(0, -1) + } + const xUpyunMoveSource = `/${this.bucket}/${oldKey}` + const uri = `/${this.bucket}/${newKey}` + const authorization = this.authorization(method, uri, '', this.operator, this.password) + const headers = { + Authorization: authorization, + 'X-Upyun-Move-Source': xUpyunMoveSource, + 'Content-Length': 0, + Date: new Date().toUTCString() + } + const res = await axios({ + method, + url: `http://v0.api.upyun.com${uri}`, + headers + }) + return res.status === 200 + } + + /** + * 删除文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string + * } + */ + async deleteBucketFile (configMap: IStringKeyMap): Promise { + const { key } = configMap + const res = await this.cli.deleteFile(key) + return res + } + + /** + * delete bucket folder + * @param configMap + */ + async deleteBucketFolder (configMap: IStringKeyMap): Promise { + const { key } = configMap + let marker = '' + let isTruncated + const allFileList = { + CommonPrefixes: [] as any[], + Contents: [] as any[] + } + do { + const res = await this.cli.listDir(key, { + limit: 10000, + iter: marker + }) + if (res) { + res.files.forEach((item: any) => { + item.type === 'N' && allFileList.Contents.push({ + ...item, + key: `${key}${item.name}` + }) + item.type === 'F' && allFileList.CommonPrefixes.push({ + ...item, + key: `${key}${item.name}/` + }) + }) + marker = res.next + isTruncated = res.next !== this.stopMarker + } else { + return false + } + } while (isTruncated) + if (allFileList.Contents.length > 0) { + let success = false + for (let i = 0; i < allFileList.Contents.length; i++) { + const item = allFileList.Contents[i] + success = await this.cli.deleteFile(item.key) + if (!success) { + return false + } + } + } + if (allFileList.CommonPrefixes.length > 0) { + for (const item of allFileList.CommonPrefixes) { + const res = await this.deleteBucketFolder({ + key: item.key + }) + if (!res) { + return false + } + } + } + const deleteSelf = await this.cli.deleteFile(key) + if (!deleteSelf) { + return false + } + return true + } + + /** + * upload file to bucket + * axiso:onUploadProgress not work in nodejs , use got instead + * @param configMap + */ + async uploadBucketFile (configMap: IStringKeyMap): Promise { + const { fileArray } = configMap + const instance = UpDownTaskQueue.getInstance() + fileArray.forEach((item: any) => { + item.key = item.key.replace(/^\/+/, '') + }) + for (const item of fileArray) { + const { bucketName, region, key, filePath, fileName, fileSize } = item + const id = `${bucketName}-${region}-${key}-${filePath}` + if (instance.getUploadTask(id)) { + continue + } + instance.addUploadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + sourceFilePath: filePath, + targetFilePath: key, + targetFileBucket: bucketName, + targetFileRegion: region + }) + const date = new Date().toUTCString() + const uri = `/${key}` + const method = 'POST' + const uplpadPolicy = { + bucket: bucketName, + 'save-key': uri, + expiration: Math.floor(Date.now() / 1000) + 2592000, + date, + 'content-length': fileSize + } + const base64Policy = Buffer.from(JSON.stringify(uplpadPolicy)).toString('base64') + const stringToSign = `${method}&/${bucketName}&${date}&${base64Policy}` + const signature = hmacSha1Base64(md5(this.password, 'hex'), stringToSign) + const authorization = `UPYUN ${this.operator}:${signature}` + const form = new FormData() + form.append('policy', base64Policy) + form.append('authorization', authorization) + form.append('file', fs.createReadStream(filePath), { + filename: path.basename(key), + contentType: getFileMimeType(fileName) + }) + const headers = form.getHeaders() + headers.Host = 'v0.api.upyun.com' + headers.Date = date + headers.Authorization = authorization + gotUpload(instance, `http://v0.api.upyun.com/${bucketName}`, method, form, headers, id, this.logger) + } + return true + } + + /** + * 新建文件夹 + * @param configMap + */ + async createBucketFolder (configMap: IStringKeyMap): Promise { + const { key } = configMap + const res = await this.cli.makeDir(`/${key}`) + return res + } + + /** + * 下载文件 + * @param configMap + */ + async downloadBucketFile (configMap: IStringKeyMap): Promise { + const { downloadPath, fileArray } = configMap + const instance = UpDownTaskQueue.getInstance() + for (const item of fileArray) { + const { bucketName, region, key, fileName, customUrl } = item + const savedFilePath = path.join(downloadPath, fileName) + const fileStream = fs.createWriteStream(savedFilePath) + const id = `${bucketName}-${region}-${key}` + if (instance.getDownloadTask(id)) { + continue + } + instance.addDownloadTask({ + id: `${bucketName}-${region}-${key}`, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + targetFilePath: savedFilePath + }) + const preSignedUrl = `${customUrl}/${key}` + gotDownload(instance, preSignedUrl, fileStream, id, savedFilePath, this.logger) + } + return true + } +} + +export default UpyunApi diff --git a/src/main/manage/datastore/db.ts b/src/main/manage/datastore/db.ts new file mode 100644 index 0000000..a48b614 --- /dev/null +++ b/src/main/manage/datastore/db.ts @@ -0,0 +1,66 @@ +/* eslint-disable */ +import { JSONStore } from '@picgo/store' +import { IJSON } from '@picgo/store/dist/types' +import { ManageApiType, ManageConfigType } from '~/universal/types/manage' + +class ManageDB { + private readonly ctx: ManageApiType + private readonly db: JSONStore + constructor (ctx: ManageApiType) { + this.ctx = ctx + this.db = new JSONStore(this.ctx.configPath) + let initParams: IStringKeyMap = { + picBed: {}, + settings: {}, + currentPicBed: 'placeholder' + } + for (let key in initParams) { + if (!this.db.has(key)) { + try { + this.db.set(key, initParams[key]) + } catch (e: any) { + this.ctx.logger.error(e) + throw e + } + } + } + } + + read (flush?: boolean): IJSON { + return this.db.read(flush) + } + + get (key: string = ''): any { + this.read(true) + return this.db.get(key) + } + + set (key: string, value: any): void { + this.read(true) + return this.db.set(key, value) + } + + has (key: string): boolean { + this.read(true) + return this.db.has(key) + } + + unset (key: string, value: any): boolean { + this.read(true) + return this.db.unset(key, value) + } + + saveConfig (config: Partial): void { + Object.keys(config).forEach((name: string) => { + this.set(name, config[name]) + }) + } + + removeConfig (config: ManageConfigType): void { + Object.keys(config).forEach((name: string) => { + this.unset(name, config[name]) + }) + } +} + +export default ManageDB diff --git a/src/main/manage/datastore/dbChecker.ts b/src/main/manage/datastore/dbChecker.ts new file mode 100644 index 0000000..3c81376 --- /dev/null +++ b/src/main/manage/datastore/dbChecker.ts @@ -0,0 +1,116 @@ +import fs from 'fs-extra' +import writeFile from 'write-file-atomic' +import path from 'path' +import { app } from 'electron' +import { getLogger } from '@core/utils/localLogger' +import dayjs from 'dayjs' +import { T } from '~/main/i18n' + +const STORE_PATH = app.getPath('userData') +const manageConfigFilePath = path.join(STORE_PATH, 'manage.json') +export const defaultManageConfigPath = manageConfigFilePath +const manageConfigFileBackupPath = path.join(STORE_PATH, 'manage.bak.json') +let _configFilePath = '' +let hasCheckPath = false + +const errorMsg = { + broken: T('TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT'), + brokenButBackup: T('TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP') +} + +/** ensure notification list */ +if (!global.notificationList) global.notificationList = [] + +function manageDbChecker () { + if (process.type !== 'renderer') { + const manageConfigFilePath = managePathChecker() + if (!fs.existsSync(manageConfigFilePath)) { + return + } + let configFile: string = '{}' + const optionsTpl = { + title: T('TIPS_NOTICE'), + body: '' + } + // config save bak + try { + configFile = fs.readFileSync(manageConfigFilePath, { encoding: 'utf-8' }) + JSON.parse(configFile) + } catch (e) { + fs.unlinkSync(manageConfigFilePath) + if (fs.existsSync(manageConfigFileBackupPath)) { + try { + configFile = fs.readFileSync(manageConfigFileBackupPath, { encoding: 'utf-8' }) + JSON.parse(configFile) + writeFile.sync(manageConfigFilePath, configFile, { encoding: 'utf-8' }) + const stats = fs.statSync(manageConfigFileBackupPath) + optionsTpl.body = `${errorMsg.brokenButBackup}\n${T('TIPS_PICGO_BACKUP_FILE_VERSION', { + v: dayjs(stats.mtime).format('YYYY-MM-DD HH:mm:ss') + })}` + global.notificationList.push(optionsTpl) + return + } catch (e) { + optionsTpl.body = errorMsg.broken + global.notificationList.push(optionsTpl) + return + } + } + optionsTpl.body = errorMsg.broken + global.notificationList.push(optionsTpl) + return + } + writeFile.sync(manageConfigFileBackupPath, configFile, { encoding: 'utf-8' }) + } +} + +/** + * Get manage config path + */ +function managePathChecker (): string { + if (_configFilePath) { + return _configFilePath + } + // defaultConfigPath + _configFilePath = defaultManageConfigPath + // if defaultConfig path is not exit + // do not parse the content of config + if (!fs.existsSync(defaultManageConfigPath)) { + return _configFilePath + } + try { + const configString = fs.readFileSync(defaultManageConfigPath, { encoding: 'utf-8' }) + const config = JSON.parse(configString) + const userConfigPath: string = config.configPath || '' + if (userConfigPath) { + if (fs.existsSync(userConfigPath) && userConfigPath.endsWith('.json')) { + _configFilePath = userConfigPath + return _configFilePath + } + } + return _configFilePath + } catch (e) { + const manageLogPath = path.join(STORE_PATH, 'manage-gui-local.log') + const logger = getLogger(manageLogPath, 'Manage') + if (!hasCheckPath) { + const optionsTpl = { + title: T('TIPS_NOTICE'), + body: T('TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR') + } + global.notificationList?.push(optionsTpl) + hasCheckPath = true + } + logger('error', e) + _configFilePath = defaultManageConfigPath + return _configFilePath + } +} + +function managePathDir () { + return path.dirname(managePathChecker()) +} + +export { + managePathChecker, + managePathDir, + manageDbChecker +} diff --git a/src/main/manage/datastore/upDownTaskQueue.ts b/src/main/manage/datastore/upDownTaskQueue.ts new file mode 100644 index 0000000..019d824 --- /dev/null +++ b/src/main/manage/datastore/upDownTaskQueue.ts @@ -0,0 +1,212 @@ +// a singleton class to manage the up/down task queue +// qiniu tcyun aliyun smms imgur github upyun + +import path from 'path' +import { app } from 'electron' +import fs from 'fs-extra' +export enum commonTaskStatus { + queuing = 'queuing', + failed = 'failed', + canceled = 'canceled', + paused = 'paused' +} + +export enum uploadTaskSpecialStatus { + uploading = 'uploading', + uploaded = 'uploaded' +} + +export enum downloadTaskSpecialStatus { + downloading = 'downloading', + downloaded = 'downloaded', +} + +export type uploadTaskStatus = commonTaskStatus | uploadTaskSpecialStatus +type downloadTaskStatus = commonTaskStatus | downloadTaskSpecialStatus + +export interface IUploadTask { + id: string + progress: number + status: uploadTaskStatus + sourceFilePath: string + sourceFileName: string + targetFilePath: string + targetFileBucket?: string + response?: any + cancelToken?: string + timeConsuming?: number + alias?: string + [other: string]: any +} + +export interface IDownloadTask { + id: string + progress: number + status: downloadTaskStatus + sourceFileUrl?: string + sourceFileName?: string + sourceConfig?: IStringKeyMap + targetFilePath?: string + response?: any + cancelToken?: string + timeConsuming?: number + reseumConfig?: IStringKeyMap + alias?: string + [other: string]: any +} + +class UpDownTaskQueue { + /* eslint-disable */ + private static instance: UpDownTaskQueue + /* eslint-enable */ + private uploadTaskQueue = [] + + private downloadTaskQueue = [] + + private persistPath = path.join(app.getPath('userData'), 'UpDownTaskQueue.json') + + private constructor () { + this.restore() + } + + public static getInstance () { + if (!UpDownTaskQueue.instance) { + UpDownTaskQueue.instance = new UpDownTaskQueue() + } + return UpDownTaskQueue.instance + } + + getUploadTaskQueue () { + return UpDownTaskQueue.getInstance().uploadTaskQueue + } + + getDownloadTaskQueue () { + return UpDownTaskQueue.getInstance().downloadTaskQueue + } + + getUploadTask (taskId: string) { + return UpDownTaskQueue.getInstance().uploadTaskQueue.find(item => item.id === taskId) + } + + getAllUploadTask () { + return UpDownTaskQueue.getInstance().uploadTaskQueue + } + + addUploadTask (task: IUploadTask) { + UpDownTaskQueue.getInstance().uploadTaskQueue.push(task) + } + + updateUploadTask (task: Partial) { + const taskIndex = UpDownTaskQueue.getInstance().uploadTaskQueue.findIndex(item => item.id === task.id) + if (taskIndex !== -1) { + const taskKeys = Object.keys(task) + taskKeys.forEach(key => { + if (key !== 'id') { + UpDownTaskQueue.getInstance().uploadTaskQueue[taskIndex][key] = task[key] + } + }) + } + } + + removeUploadTask (taskId: string) { + const taskIndex = UpDownTaskQueue.getInstance().uploadTaskQueue.findIndex(item => item.id === taskId) + if (taskIndex !== -1) { + UpDownTaskQueue.getInstance().uploadTaskQueue.splice(taskIndex, 1) + } + } + + removeDownloadTask (taskId: string) { + const taskIndex = UpDownTaskQueue.getInstance().downloadTaskQueue.findIndex(item => item.id === taskId) + if (taskIndex !== -1) { + UpDownTaskQueue.getInstance().downloadTaskQueue.splice(taskIndex, 1) + } + } + + getDownloadTask (taskId: string) { + return UpDownTaskQueue.getInstance().downloadTaskQueue.find(item => item.id === taskId) + } + + getAllDownloadTask () { + return UpDownTaskQueue.getInstance().downloadTaskQueue + } + + addDownloadTask (task: IDownloadTask) { + UpDownTaskQueue.getInstance().downloadTaskQueue.push(task) + } + + updateDownloadTask (task: Partial) { + const taskIndex = UpDownTaskQueue.getInstance().downloadTaskQueue.findIndex(item => item.id === task.id) + if (taskIndex !== -1) { + const taskKeys = Object.keys(task) + taskKeys.forEach(key => { + if (key !== 'id') { + UpDownTaskQueue.getInstance().downloadTaskQueue[taskIndex][key] = task[key] + } + }) + } + } + + clearUploadTaskQueue () { + UpDownTaskQueue.getInstance().uploadTaskQueue = [] + } + + removeUploadedTask () { + UpDownTaskQueue.getInstance().uploadTaskQueue = UpDownTaskQueue.getInstance().uploadTaskQueue.filter(item => item.status !== uploadTaskSpecialStatus.uploaded && item.status !== commonTaskStatus.canceled && item.status !== commonTaskStatus.failed) + } + + removeDownloadedTask () { + UpDownTaskQueue.getInstance().downloadTaskQueue = UpDownTaskQueue.getInstance().downloadTaskQueue.filter(item => item.status !== downloadTaskSpecialStatus.downloaded && item.status !== commonTaskStatus.canceled && item.status !== commonTaskStatus.failed) + } + + clearDownloadTaskQueue () { + UpDownTaskQueue.getInstance().downloadTaskQueue = [] + } + + clearAllTaskQueue () { + this.clearUploadTaskQueue() + this.clearDownloadTaskQueue() + } + + persist () { + try { + this.checkPersistPath() + fs.writeFileSync(this.persistPath, JSON.stringify({ + uploadTaskQueue: this.uploadTaskQueue, + downloadTaskQueue: this.downloadTaskQueue + })) + } catch (e) { + console.log(e) + } + } + + private restore () { + try { + this.checkPersistPath() + const persistData = JSON.parse(fs.readFileSync(this.persistPath, { encoding: 'utf-8' })) + this.uploadTaskQueue = persistData.uploadTaskQueue + this.downloadTaskQueue = persistData.downloadTaskQueue + } catch (e) { + this.uploadTaskQueue = [] + this.downloadTaskQueue = [] + } + } + + private checkPersistPath () { + if (!fs.existsSync(this.persistPath)) { + fs.writeFileSync(this.persistPath, JSON.stringify({ + uploadTaskQueue: this.uploadTaskQueue, + downloadTaskQueue: this.downloadTaskQueue + })) + } + try { + JSON.parse(fs.readFileSync(this.persistPath, { encoding: 'utf-8' })) + } catch (e) { + fs.writeFileSync(this.persistPath, JSON.stringify({ + uploadTaskQueue: this.uploadTaskQueue, + downloadTaskQueue: this.downloadTaskQueue + })) + } + } +} + +export default UpDownTaskQueue diff --git a/src/main/manage/events/constants.ts b/src/main/manage/events/constants.ts new file mode 100644 index 0000000..d1c05f5 --- /dev/null +++ b/src/main/manage/events/constants.ts @@ -0,0 +1,3 @@ +export const PICLIST_MANAGE_GET_CONFIG = 'PICLIST_MANAGE_GET_CONFIG' +export const PICLIST_MANAGE_SAVE_CONFIG = 'PICLIST_MANAGE_SAVE_CONFIG' +export const PICLIST_MANAGE_REMOVE_CONFIG = 'PICLIST_MANAGE_REMOVE_CONFIG' diff --git a/src/main/manage/events/ipcList.ts b/src/main/manage/events/ipcList.ts new file mode 100644 index 0000000..d52d436 --- /dev/null +++ b/src/main/manage/events/ipcList.ts @@ -0,0 +1,142 @@ +import manageCoreIPC from './manageCoreIPC' +import { ManageApi } from '../manageApi' +import { ipcMain, IpcMainInvokeEvent, dialog, app, shell } from 'electron' +import UpDownTaskQueue from '../datastore/upDownTaskQueue' +import { downloadFileFromUrl } from '../utils/common' +import path from 'path' +import fs from 'fs-extra' + +export const manageIpcList = { + listen () { + manageCoreIPC.listen() + + ipcMain.handle('getBucketList', async (_evt: IpcMainInvokeEvent, currentPicBed: string) => { + const manage = new ManageApi(currentPicBed) + return manage.getBucketList() + }) + + ipcMain.handle('createBucket', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + return manage.createBucket(param) + }) + + ipcMain.handle('getBucketFileList', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + return manage.getBucketFileList(param) + }) + + ipcMain.handle('getBucketDomain', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + const result = await manage.getBucketDomain(param) + return result + }) + + ipcMain.handle('setBucketAclPolicy', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + return manage.setBucketAclPolicy(param) + }) + + ipcMain.handle('renameBucketFile', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + return manage.renameBucketFile(param) + }) + + ipcMain.handle('deleteBucketFile', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + return manage.deleteBucketFile(param) + }) + + ipcMain.handle('deleteBucketFolder', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + return manage.deleteBucketFolder(param) + }) + + ipcMain.on('getBucketListBackstage', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + return manage.getBucketListBackstage(param) + }) + + ipcMain.handle('openFileSelectDialog', async () => { + const res = await dialog.showOpenDialog({ + properties: ['openFile', 'multiSelections'] + }) + if (res.canceled) { + return [] + } else { + return res.filePaths + } + }) + + ipcMain.handle('getPreSignedUrl', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + return manage.getPreSignedUrl(param) + }) + + ipcMain.handle('getUploadTaskList', async () => { + return UpDownTaskQueue.getInstance().getAllUploadTask() + }) + + ipcMain.handle('getDownloadTaskList', async () => { + return UpDownTaskQueue.getInstance().getAllDownloadTask() + }) + + ipcMain.on('uploadBucketFile', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + return manage.uploadBucketFile(param) + }) + + ipcMain.on('downloadBucketFile', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + return manage.downloadBucketFile(param) + }) + + ipcMain.handle('createBucketFolder', async (_evt: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => { + const manage = new ManageApi(currentPicBed) + return manage.createBucketFolder(param) + }) + + ipcMain.on('deleteUploadedTask', async () => { + UpDownTaskQueue.getInstance().removeUploadedTask() + }) + + ipcMain.on('deleteAllUploadedTask', async () => { + UpDownTaskQueue.getInstance().clearUploadTaskQueue() + }) + + ipcMain.on('deleteDownloadedTask', async () => { + UpDownTaskQueue.getInstance().removeDownloadedTask() + }) + + ipcMain.on('deleteAllDownloadedTask', async () => { + UpDownTaskQueue.getInstance().clearDownloadTaskQueue() + }) + + ipcMain.handle('selectDownloadFolder', async () => { + const res = await dialog.showOpenDialog({ + properties: ['openDirectory'] + }) + return res.filePaths[0] + }) + + ipcMain.handle('getDefaultDownloadFolder', async () => { + return app.getPath('downloads') + }) + + ipcMain.on('OpenDownloadedFolder', async (_evt: IpcMainInvokeEvent, path: string | undefined) => { + if (path) { + shell.showItemInFolder(path) + } else { + shell.openPath(app.getPath('downloads')) + } + }) + + ipcMain.on('OpenLocalFile', async (_evt: IpcMainInvokeEvent, fullPath: string) => { + fs.existsSync(fullPath) ? shell.showItemInFolder(fullPath) : shell.openPath(path.dirname(fullPath)) + }) + + ipcMain.handle('downloadFileFromUrl', async (_evt: IpcMainInvokeEvent, urls: string[]) => { + const res = await downloadFileFromUrl(urls) + return res + }) + } +} diff --git a/src/main/manage/events/manageCoreIPC.ts b/src/main/manage/events/manageCoreIPC.ts new file mode 100644 index 0000000..696ef0f --- /dev/null +++ b/src/main/manage/events/manageCoreIPC.ts @@ -0,0 +1,35 @@ +import { + IpcMainEvent, + ipcMain +} from 'electron' +import getManageApi from '../Main' +import { PICLIST_MANAGE_GET_CONFIG, PICLIST_MANAGE_SAVE_CONFIG, PICLIST_MANAGE_REMOVE_CONFIG } from './constants' + +const manageApi = getManageApi() + +const handleManageGetConfig = () => { + ipcMain.on(PICLIST_MANAGE_GET_CONFIG, (event: IpcMainEvent, key: string | undefined, callbackId: string) => { + const result = manageApi.getConfig(key) + event.sender.send(PICLIST_MANAGE_GET_CONFIG, result, callbackId) + }) +} + +const handleManageSaveConfig = () => { + ipcMain.on(PICLIST_MANAGE_SAVE_CONFIG, (_event: IpcMainEvent, data: any) => { + manageApi.saveConfig(data) + }) +} + +const handleManageRemoveConfig = () => { + ipcMain.on(PICLIST_MANAGE_REMOVE_CONFIG, (_event: IpcMainEvent, key: string, propName: string) => { + manageApi.removeConfig(key, propName) + }) +} + +export default { + listen () { + handleManageGetConfig() + handleManageSaveConfig() + handleManageRemoveConfig() + } +} diff --git a/src/main/manage/manageApi.ts b/src/main/manage/manageApi.ts new file mode 100644 index 0000000..dc7e383 --- /dev/null +++ b/src/main/manage/manageApi.ts @@ -0,0 +1,529 @@ + +import fs from 'fs-extra' +import path from 'path' +import { EventEmitter } from 'events' +import { managePathChecker } from './datastore/dbChecker' +import { + ManageApiType, + ManageConfigType, + ManageError, + PicBedMangeConfig +} from '~/universal/types/manage' +import ManageDB from './datastore/db' +import { ManageLogger } from './utils/logger' +import { get, set, unset } from 'lodash' +import { homedir } from 'os' +import { isInputConfigValid, formatError } from './utils/common' +import API from './apis/api' +import windowManager from 'apis/app/window/windowManager' +import { IWindowList } from '#/types/enum' +import { ipcMain } from 'electron' + +export class ManageApi extends EventEmitter implements ManageApiType { + private _config!: Partial + private db!: ManageDB + currentPicBed: string + configPath: string + baseDir!: string + logger: ManageLogger + currentPicBedConfig: PicBedMangeConfig + + constructor (currentPicBed: string = '') { + super() + this.currentPicBed = currentPicBed || (this.getConfig('currentPicBed') ?? 'placeholder') + this.configPath = managePathChecker() + this.initConfigPath() + this.logger = new ManageLogger(this) + this.initconfig() + this.currentPicBedConfig = this.getPicBedConfig(this.currentPicBed) + } + + getMsgParam (method: string) { + return { + class: 'ManageApi', + method, + picbedName: this.currentPicBedConfig.picBedName + } + } + + errorMsg (err: any, param: IStringKeyMap) { + this.logger.error(formatError(err, param)) + } + + createClient () { + const name = this.currentPicBedConfig.picBedName + switch (name) { + case 'tcyun': + return new API.TcyunApi(this.currentPicBedConfig.secretId, this.currentPicBedConfig.secretKey, this.logger) + case 'aliyun': + return new API.AliyunApi(this.currentPicBedConfig.accessKeyId, this.currentPicBedConfig.accessKeySecret, this.logger) + case 'qiniu': + return new API.QiniuApi(this.currentPicBedConfig.accessKey, this.currentPicBedConfig.secretKey, this.logger) + case 'upyun': + return new API.UpyunApi(this.currentPicBedConfig.bucketName, this.currentPicBedConfig.operator, this.currentPicBedConfig.password, this.logger) + case 'smms': + return new API.SmmsApi(this.currentPicBedConfig.token, this.logger) + case 'github': + return new API.GithubApi(this.currentPicBedConfig.token, this.currentPicBedConfig.githubUsername, this.currentPicBedConfig.proxy, this.logger) + case 'imgur': + return new API.ImgurApi(this.currentPicBedConfig.imgurUserName, this.currentPicBedConfig.accessToken, this.currentPicBedConfig.proxy, this.logger) + default: + return {} as any + } + } + + private getPicBedConfig (picBedName: string): PicBedMangeConfig { + return this.getConfig(`picBed.${picBedName}`) + } + + private initConfigPath (): void { + if (this.configPath === '') { + this.configPath = `${homedir()}/.piclist/manage.json` + } + if (path.extname(this.configPath).toUpperCase() !== '.JSON') { + this.configPath = '' + throw Error('The configuration file only supports JSON format.') + } + this.baseDir = path.dirname(this.configPath) + const exist = fs.pathExistsSync(this.configPath) + if (!exist) { + fs.ensureFileSync(this.configPath) + } + } + + private initconfig (): void { + this.db = new ManageDB(this) + this._config = this.db.read(true) as ManageConfigType + } + + getConfig (name?: string): T { + if (!name) { + return this._config as unknown as T + } else { + return get(this._config, name) + } + } + + saveConfig (config: IStringKeyMap): void { + if (!isInputConfigValid(config)) { + this.logger.warn( + 'the format of config is invalid, please provide object' + ) + return + } + this.setConfig(config) + this.db.saveConfig(config) + } + + removeConfig (key: string, propName: string): void { + if (!key || !propName) { + return + } + this.unsetConfig(key, propName) + this.db.unset(key, propName) + } + + setConfig (config: IStringKeyMap): void { + if (!isInputConfigValid(config)) { + this.logger.warn( + 'the format of config is invalid, please provide object' + ) + return + } + Object.keys(config).forEach((name: string) => { + set(this._config, name, config[name]) + }) + } + + unsetConfig (key: string, propName: string): void { + if (!key || !propName) return + unset(this.getConfig(key), propName) + } + + async getBucketList ( + param?: IStringKeyMap | undefined + ): Promise { + let client + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + case 'github': + case 'imgur': + try { + client = this.createClient() + return await client.getBucketList() + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('getBucketList')) + return [] + } + case 'upyun': + return [{ + Name: this.currentPicBedConfig.bucketName, + Location: 'upyun', + CreationDate: new Date().toISOString() + }] + case 'smms': + return [{ + Name: 'smms', + Location: 'smms', + CreationDate: new Date().toISOString() + }] + default: + console.log(param) + return [] + } + } + + async getBucketInfo ( + param?: IStringKeyMap | undefined + ): Promise { + console.log(param) + return {} + } + + async getBucketDomain ( + param: IStringKeyMap + ): Promise { + let client + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + case 'github': + try { + client = this.createClient() as any + return await client.getBucketDomain(param) + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('getBucketDomain')) + return [] + } + case 'upyun': + return [this.currentPicBedConfig.customUrl] + case 'smms': + return ['https://smms.app'] + case 'imgur': + return ['https://imgur.com'] + default: + return [] + } + } + + async createBucket ( + param?: IStringKeyMap + ): Promise { + let client + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + try { + client = this.createClient() as any + return await client.createBucket(param!) + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('createBucket')) + return false + } + default: + return false + } + } + + async deleteBucket ( + param?: IStringKeyMap + ): Promise { + console.log(param) + return false + } + + async getOperatorList ( + param?: IStringKeyMap + ): Promise { + console.log(param) + return [] + } + + async addOperator ( + param?: IStringKeyMap + ): Promise { + console.log(param) + return false + } + + async deleteOperator ( + param?: IStringKeyMap + ): Promise { + console.log(param) + return false + } + + async getBucketAclPolicy ( + param?: IStringKeyMap + ): Promise { + console.log(param) + return {} + } + + async setBucketAclPolicy ( + param?: IStringKeyMap + ): Promise { + let client + switch (this.currentPicBedConfig.picBedName) { + case 'qiniu': + try { + client = new API.QiniuApi(this.currentPicBedConfig.accessKey, this.currentPicBedConfig.secretKey, this.logger) + return await client.setBucketAclPolicy(param!) + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('setBucketAclPolicy')) + return false + } + default: + return false + } + } + + /** + * 后台更新bucket文件列表 + * @param param + * @returns + */ + async getBucketListBackstage ( + param?: IStringKeyMap + ): Promise { + let client + let window + const defaultResult = { + fullList: [], + success: false, + finished: true + } + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + case 'upyun': + case 'smms': + case 'github': + case 'imgur': + try { + client = this.createClient() as any + return await client.getBucketListBackstage(param!) + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('getBucketListBackstage')) + window = windowManager.get(IWindowList.SETTING_WINDOW)! + window.webContents.send('refreshFileTransferList', defaultResult) + ipcMain.removeAllListeners('cancelLoadingFileList') + return {} + } + default: + window = windowManager.get(IWindowList.SETTING_WINDOW)! + window.webContents.send('refreshFileTransferList', defaultResult) + ipcMain.removeAllListeners('cancelLoadingFileList') + return {} + } + } + + /** + * 获取文件夹列表 + * 结果统一进行格式化 文件夹提取到最前 + * key: 完整路径 + * fileName: 文件名 + * formatedTime: 格式化时间 + * isDir: 是否是文件夹 + * fileSize: 文件大小 + **/ + async getBucketFileList ( + param?: IStringKeyMap + ): Promise { + const defaultResponse = { + fullList: [], + isTruncated: false, + nextMarker: '', + success: false + } + let client + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + case 'upyun': + case 'smms': + try { + client = this.createClient() + return await client.getBucketFileList(param!) + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('getBucketFileList')) + return defaultResponse + } + default: + return defaultResponse + } + } + + async deleteBucketFile ( + param?: IStringKeyMap + ): Promise { + let client + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + case 'upyun': + case 'smms': + case 'github': + case 'imgur': + try { + client = this.createClient() as any + const res = await client.deleteBucketFile(param!) + return res + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('deleteBucketFile')) + return false + } + default: + return false + } + } + + async deleteBucketFolder ( + param?: IStringKeyMap + ): Promise { + let client + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + case 'upyun': + case 'github': + try { + client = this.createClient() as any + return await client.deleteBucketFolder(param!) + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('deleteBucketFolder')) + return false + } + default: + return false + } + } + + async renameBucketFile ( + param?: IStringKeyMap + ): Promise { + let client + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + case 'upyun': + try { + client = this.createClient() as any + return await client.renameBucketFile(param!) + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('renameBucketFile')) + return false + } + default: + return false + } + } + + async downloadBucketFile ( + param?: IStringKeyMap + ): Promise { + let client + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + case 'upyun': + case 'smms': + case 'github': + case 'imgur': + try { + client = this.createClient() as any + const res = await client.downloadBucketFile(param!) + return res + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('downloadBucketFile')) + return false + } + default: + return false + } + } + + async copyMoveBucketFile ( + param?: IStringKeyMap + ): Promise { + console.log(param) + return false + } + + async createBucketFolder ( + param?: IStringKeyMap + ): Promise { + let client + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + case 'upyun': + case 'github': + try { + client = this.createClient() as any + return await client.createBucketFolder(param!) + } catch (error) { + this.errorMsg(error, this.getMsgParam('createBucketFolder')) + return false + } + default: + return false + } + } + + async uploadBucketFile ( + param?: IStringKeyMap + ): Promise { + let client + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + case 'upyun': + case 'smms': + case 'github': + case 'imgur': + try { + client = this.createClient() as any + return await client.uploadBucketFile(param!) + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('uploadBucketFile')) + return false + } + default: + return false + } + } + + async getPreSignedUrl ( + param?: IStringKeyMap + ): Promise { + let client + switch (this.currentPicBedConfig.picBedName) { + case 'tcyun': + case 'aliyun': + case 'qiniu': + case 'github': + try { + client = this.createClient() as any + return await client.getPreSignedUrl(param!) + } catch (error: any) { + this.errorMsg(error, this.getMsgParam('getPreSignedUrl')) + return 'error' + } + default: + return 'error' + } + } +} diff --git a/src/main/manage/utils/common.ts b/src/main/manage/utils/common.ts new file mode 100644 index 0000000..8e2728e --- /dev/null +++ b/src/main/manage/utils/common.ts @@ -0,0 +1,272 @@ +import fs from 'fs-extra' +import path from 'path' +import mime from 'mime-types' +import axios from 'axios' +import { app } from 'electron' +import crypto from 'crypto' +import got, { RequestError } from 'got' +import { Stream } from 'stream' +import { promisify } from 'util' +import UpDownTaskQueue, +{ + uploadTaskSpecialStatus, + commonTaskStatus, + downloadTaskSpecialStatus +} from '../datastore/upDownTaskQueue' +import { ManageLogger } from '../utils/logger' +import { formatHttpProxy } from '@/manage/utils/common' +import { HttpsProxyAgent, HttpProxyAgent } from 'hpagent' + +export const getFSFile = async ( + filePath: string, + stream: boolean = false +): Promise => { + try { + return { + extension: path.extname(filePath), + fileName: path.basename(filePath), + buffer: stream + ? fs.createReadStream(filePath) + : await fs.readFile(filePath), + success: true + } + } catch (e) { + return { + success: false + } + } +} + +export const isInputConfigValid = (config: any): boolean => { + if ( + typeof config === 'object' && + !Array.isArray(config) && + Object.keys(config).length > 0 + ) { + return true + } + return false +} + +export const getFileMimeType = (filePath: string): string => { + return mime.lookup(filePath) || 'application/octet-stream' +} + +const checkTempFolderExist = async () => { + const tempPath = path.join(app.getPath('downloads'), 'piclistTemp') + try { + await fs.access(tempPath) + } catch (e) { + await fs.mkdir(tempPath) + } +} + +export const downloadFileFromUrl = async (urls: string[]) => { + const tempPath = path.join(app.getPath('downloads'), 'piclistTemp') + await checkTempFolderExist() + const result = [] as string[] + for (let i = 0; i < urls.length; i++) { + const finishDownload = promisify(Stream.finished) + const fileName = path.basename(urls[i]).split('?')[0] + const filePath = path.join(tempPath, fileName) + const writer = fs.createWriteStream(filePath) + const res = await axios({ + method: 'get', + url: urls[i], + responseType: 'stream' + }) + res.data.pipe(writer) + await finishDownload(writer) + result.push(filePath) + } + return result +} + +export const clearTempFolder = () => fs.emptyDirSync(path.join(app.getPath('downloads'), 'piclistTemp')) + +export const md5 = (str: string, code: 'hex' | 'base64'): string => crypto.createHash('md5').update(str).digest(code) + +export const hmacSha1Base64 = (secretKey: string, stringToSign: string) : string => crypto.createHmac('sha1', secretKey).update(Buffer.from(stringToSign, 'utf8')).digest('base64') + +export const gotDownload = async ( + instance: UpDownTaskQueue, + preSignedUrl: string, + fileStream: fs.WriteStream, + id : string, + savedFilePath: string, + logger?: ManageLogger, + param?: any, + agent: any = {} +) => { + got( + preSignedUrl, + { + timeout: { + request: 30000 + }, + isStream: true, + throwHttpErrors: false, + searchParams: param, + agent + } + ) + .on('downloadProgress', (progress: any) => { + instance.updateDownloadTask({ + id, + progress: Math.floor(progress.percent * 100), + status: downloadTaskSpecialStatus.downloading + }) + }) + .pipe(fileStream) + .on('finish', () => { + instance.updateDownloadTask({ + id, + progress: 100, + status: downloadTaskSpecialStatus.downloaded, + finishTime: new Date().toLocaleString() + }) + }) + .on('error', (err: any) => { + logger && logger.error(formatError(err, { method: 'gotDownload' })) + fs.remove(savedFilePath) + instance.updateDownloadTask({ + id, + progress: 0, + status: commonTaskStatus.failed, + response: formatError(err, { method: 'gotDownload' }), + finishTime: new Date().toLocaleString() + }) + }) +} + +export const gotUpload = async ( + instance: UpDownTaskQueue, + url: string, + method: 'PUT' | 'POST', + body: any, + headers: any, + id: string, + logger?: ManageLogger, + timeout: number = 30000, + throwHttpErrors: boolean = false, + agent: any = {} +) => { + got( + url, + { + headers, + method, + body, + timeout: { + request: timeout + }, + throwHttpErrors, + agent + } + ) + .on('uploadProgress', (progress: any) => { + instance.updateUploadTask({ + id, + progress: Math.floor(progress.percent * 100), + status: uploadTaskSpecialStatus.uploading + }) + }) + .then((res: any) => { + instance.updateUploadTask({ + id, + progress: res && (res.statusCode === 200 || res.statusCode === 201) ? 100 : 0, + status: res && (res.statusCode === 200 || res.statusCode === 201) ? uploadTaskSpecialStatus.uploaded : commonTaskStatus.failed, + finishTime: new Date().toLocaleString() + }) + }) + .catch((err: any) => { + logger && logger.error(formatError(err, { method: 'gotUpload' })) + instance.updateUploadTask({ + id, + progress: 0, + response: formatError(err, { method: 'gotUpload' }), + status: commonTaskStatus.failed, + finishTime: new Date().toLocaleString() + }) + }) +} + +export const formatError = (err: any, params:IStringKeyMap) => { + if (err instanceof RequestError) { + return { + ...params, + message: err.message ?? '', + name: 'RequestError', + code: err.code, + stack: err.stack ?? '', + timings: err.timings ?? {} + } + } else if (err instanceof Error) { + return { + ...params, + name: err.name ?? '', + message: err.message ?? '', + stack: err.stack ?? '' + } + } else { + if (typeof err === 'object') { + return JSON.stringify(err) + JSON.stringify(params) + } else { + return String(err) + JSON.stringify(params) + } + } +} + +export const trimPath = (path: string) => path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/') + +export const getAgent = (proxy:any, https: boolean = true) => { + const formatProxy = formatHttpProxy(proxy, 'string') as any + const opt = { + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + scheduling: 'lifo' as 'lifo' | 'fifo' | undefined, + proxy: formatProxy.replace('127.0.0.1', 'localhost') + } + if (https) { + return formatProxy + ? { + https: new HttpsProxyAgent(opt) + } + : {} + } else { + return formatProxy + ? { + http: new HttpProxyAgent(opt) + } + : {} + } +} + +export function getOptions ( + method?: string, + headers?: IStringKeyMap, + searchParams?: IStringKeyMap, + responseType?: string, + body?: any, + timeout?: number, + proxy?: any +) { + const options = { + method: method?.toUpperCase(), + headers, + searchParams, + agent: getAgent(proxy), + timeout: { + request: timeout || 30000 + }, + body, + throwHttpErrors: false, + responseType + } as IStringKeyMap + Object.keys(options).forEach(key => { + options[key] === undefined && delete options[key] + }) + return options +} diff --git a/src/main/manage/utils/constants.ts b/src/main/manage/utils/constants.ts new file mode 100644 index 0000000..716e296 --- /dev/null +++ b/src/main/manage/utils/constants.ts @@ -0,0 +1,68 @@ +const AliyunAreaCodeName : IStringKeyMap = { + 'oss-cn-hangzhou': '华东1(杭州)', + 'oss-cn-shanghai': '华东2(上海)', + 'oss-cn-nanjing': '华东5(南京本地地域)', + 'oss-cn-fuzhou': '华东6(福州本地地域)', + 'oss-cn-qingdao': '华北1(青岛)', + 'oss-cn-beijing': '华北2(北京)', + 'oss-cn-zhangjiakou': '华北3(张家口)', + 'oss-cn-huhehaote': '华北5(呼和浩特)', + 'oss-cn-wulanchabu': '华北6(乌兰察布)', + 'oss-cn-shenzhen': '华南1(深圳)', + 'oss-cn-heyuan': '华南2(河源)', + 'oss-cn-guangzhou': '华南3(广州)', + 'oss-cn-chengdu': '西南1(成都)', + 'oss-cn-hongkong': '中国(香港)', + 'oss-us-west-1': '美国(硅谷)', + 'oss-us-east-1': '美国(弗吉尼亚)', + 'oss-ap-northeast-1': '日本(东京)', + 'oss-ap-northeast-2': '韩国(首尔)', + 'oss-ap-southeast-1': '新加坡', + 'oss-ap-southeast-2': '澳大利亚(悉尼)', + 'oss-ap-southeast-3': '马来西亚(吉隆坡)', + 'oss-ap-southeast-5': '印度尼西亚(雅加达)', + 'oss-ap-southeast-6': '菲律宾(马尼拉)', + 'oss-ap-southeast-7': '泰国(曼谷)', + 'oss-ap-south-1': '印度(孟买)', + 'oss-eu-central-1': '德国(法兰克福)', + 'oss-eu-west-1': '英国(伦敦)', + 'oss-me-east-1': '阿联酋(迪拜)' +} + +const QiniuAreaCodeName : IStringKeyMap = { + z0: '华东-浙江', + 'cn-east-2': '华东 浙江2', + z1: '华北-河北', + z2: '华南-广东', + na0: '北美-洛杉矶', + as0: '亚太-新加坡', + 'ap-northeast-1': '亚太-首尔' +} + +const TencentAreaCodeName : IStringKeyMap = { + 'ap-beijing-1': '北京一区', + 'ap-beijing': '北京', + 'ap-nanjing': '南京', + 'ap-shanghai': '上海', + 'ap-guangzhou': '广州', + 'ap-chengdu': '成都', + 'ap-chongqing': '重庆', + 'ap-shenzhen-fsi': '深圳金融', + 'ap-shagnhai-fsi': '上海金融', + 'ap-beijing-fsi': '北京金融', + 'ap-hongkong': '香港', + 'ap-singapore': '新加坡', + 'ap-mumbai': '孟买', + 'ap-jakarta': '雅加达', + 'ap-seoul': '首尔', + 'ap-bangkok': '曼谷', + 'ap-tokyo': '东京', + 'na-siliconvalley': '硅谷(美西)', + 'na-ashburn': '弗吉尼亚(美东)', + 'na-toronto': '多伦多', + 'sa-saopaulo': '圣保罗', + 'eu-frankfurt': '法兰克福', + 'eu-moscow': '莫斯科' +} + +export { AliyunAreaCodeName, QiniuAreaCodeName, TencentAreaCodeName } diff --git a/src/main/manage/utils/logger.ts b/src/main/manage/utils/logger.ts new file mode 100644 index 0000000..58991c3 --- /dev/null +++ b/src/main/manage/utils/logger.ts @@ -0,0 +1,165 @@ +import chalk from 'chalk' +import dayjs from 'dayjs' +import fs from 'fs-extra' +import path from 'path' +import util from 'util' +import { ILogType } from '#/types/enum' +import { ILogColor, ILogger } from 'piclist/dist/types' +import { ManageApiType, Undefinable } from '~/universal/types/manage' +import { enforceNumber, isDev } from '#/utils/common' + +export class ManageLogger implements ILogger { + private readonly level = { + [ILogType.success]: 'green', + [ILogType.info]: 'blue', + [ILogType.warn]: 'yellow', + [ILogType.error]: 'red' + } + + private readonly ctx: ManageApiType + private logLevel!: string + private logPath!: string + + constructor (ctx: ManageApiType) { + this.ctx = ctx + } + + private handleLog (type: ILogType, ...msg: ILogArgvTypeWithError[]): void { + const logHeader = chalk[this.level[type] as ILogColor]( + `[PicList ${type.toUpperCase()}]` + ) + console.log(logHeader, ...msg) + this.logLevel = this.ctx.getConfig('settings.logLevel') + this.logPath = + this.ctx.getConfig>('settings.logPath') || + path.join(this.ctx.baseDir, './manage.log') + setTimeout(() => { + try { + const result = this.checkLogFileIsLarge(this.logPath) + if (result.isLarge) { + const warningMsg = `Log file is too large (> ${ + result.logFileSizeLimit! / 1024 / 1024 || '10' + } MB), recreate log file` + console.log(chalk.yellow('[PicList WARN]:'), warningMsg) + this.recreateLogFile(this.logPath) + msg.unshift(warningMsg) + } + this.handleWriteLog(this.logPath, type, ...msg) + } catch (e) { + console.error('[PicList Error] on checking log file size', e) + } + }, 0) + } + + private checkLogFileIsLarge (logPath: string): { + isLarge: boolean + logFileSize?: number + logFileSizeLimit?: number + } { + if (fs.existsSync(logPath)) { + const logFileSize = fs.statSync(logPath).size + const logFileSizeLimit = + enforceNumber( + this.ctx.getConfig>( + 'settings.logFileSizeLimit' + ) || 10 + ) * + 1024 * + 1024 + return { + isLarge: logFileSize > logFileSizeLimit, + logFileSize, + logFileSizeLimit + } + } + fs.ensureFileSync(logPath) + return { + isLarge: false + } + } + + private recreateLogFile (logPath: string): void { + if (fs.existsSync(logPath)) { + fs.unlinkSync(logPath) + fs.createFileSync(logPath) + } + } + + private handleWriteLog ( + logPath: string, + type: string, + ...msg: ILogArgvTypeWithError[] + ): void { + try { + if (this.checkLogLevel(type, this.logLevel)) { + let log = `${dayjs().format( + 'YYYY-MM-DD HH:mm:ss' + )} [PicList ${type.toUpperCase()}] ` + msg.forEach((item: ILogArgvTypeWithError) => { + if (item instanceof Error && type === 'error') { + log += `\n------Error Stack Begin------\n${util.format( + item?.stack + )}\n-------Error Stack End------- ` + } else { + if (typeof item === 'object') { + if (item?.stack) { + log = log + `\n------Error Stack Begin------\n${util.format( + item.stack + )}\n-------Error Stack End------- ` + } + item = JSON.stringify(item, (key, value) => { + if (key === 'stack') { + return undefined + } + return value + }, 2) + } + log += `${item as string} ` + } + }) + log += '\n' + fs.appendFileSync(logPath, log) + } + } catch (e) { + console.error('[PicList Error] on writing log file', e) + } + } + + private checkLogLevel ( + type: string, + level: undefined | string | string[] + ): boolean { + if (level === undefined || level === 'all') { + return true + } + if (Array.isArray(level)) { + return level.some((item: string) => item === type || item === 'all') + } else { + return type === level + } + } + + success (...msq: ILogArgvType[]): void { + return this.handleLog(ILogType.success, ...msq) + } + + info (...msq: ILogArgvType[]): void { + return this.handleLog(ILogType.info, ...msq) + } + + error (...msq: ILogArgvTypeWithError[]): void { + return this.handleLog(ILogType.error, ...msq) + } + + warn (...msq: ILogArgvType[]): void { + return this.handleLog(ILogType.warn, ...msq) + } + + debug (...msq: ILogArgvType[]): void { + if (isDev) { + this.handleLog(ILogType.info, ...msq) + } + } +} + +export default ManageLogger diff --git a/src/main/migrate/index.ts b/src/main/migrate/index.ts index 97ad47f..f29bea6 100644 --- a/src/main/migrate/index.ts +++ b/src/main/migrate/index.ts @@ -2,7 +2,7 @@ import { DBStore } from '@picgo/store' import ConfigStore from '~/main/apis/core/datastore' import path from 'path' import fse from 'fs-extra' -import { PicGo as PicGoCore } from 'picgo' +import { PicGo as PicGoCore } from 'piclist' import { T } from '~/main/i18n' // from v2.1.2 const updateShortKeyFromVersion212 = (db: typeof ConfigStore, shortKeyConfig: IShortKeyConfigs | IOldShortKeyConfigs) => { diff --git a/src/main/server/index.ts b/src/main/server/index.ts index f8ac34a..a586149 100644 --- a/src/main/server/index.ts +++ b/src/main/server/index.ts @@ -48,7 +48,7 @@ class Server { if (request.method === 'POST') { if (!routers.getHandler(request.url!)) { - logger.warn(`[PicGo Server] don't support [${request.url}] url`) + logger.warn(`[PicList Server] don't support [${request.url}] url`) handleResponse({ response, statusCode: 404, @@ -66,7 +66,7 @@ class Server { try { postObj = (body === '') ? {} : JSON.parse(body) } catch (err: any) { - logger.error('[PicGo Server]', err) + logger.error('[PicList Server]', err) return handleResponse({ response, body: { @@ -75,7 +75,7 @@ class Server { } }) } - logger.info('[PicGo Server] get the request', body) + logger.info('[PicList Server] get the request', body) const handler = routers.getHandler(request.url!) handler!({ ...postObj, @@ -84,7 +84,7 @@ class Server { }) } } else { - logger.warn(`[PicGo Server] don't support [${request.method}] method`) + logger.warn(`[PicList Server] don't support [${request.method}] method`) response.statusCode = 404 response.end() } @@ -92,7 +92,7 @@ class Server { // port as string is a bug private listen = (port: number | string) => { - logger.info(`[PicGo Server] is listening at ${port}`) + logger.info(`[PicList Server] is listening at ${port}`) if (typeof port === 'string') { port = parseInt(port, 10) } @@ -103,7 +103,7 @@ class Server { await axios.post(ensureHTTPLink(`${this.config.host}:${port}/heartbeat`)) this.shutdown(true) } catch (e) { - logger.warn(`[PicGo Server] ${port} is busy, trying with port ${(port as number) + 1}`) + logger.warn(`[PicList Server] ${port} is busy, trying with port ${(port as number) + 1}`) // fix a bug: not write an increase number to config file // to solve the auto number problem this.listen((port as number) + 1) @@ -122,7 +122,7 @@ class Server { shutdown (hasStarted?: boolean) { this.httpServer.close() if (!hasStarted) { - logger.info('[PicGo Server] shutdown') + logger.info('[PicList Server] shutdown') } } diff --git a/src/main/server/routerManager.ts b/src/main/server/routerManager.ts index 5d58922..3504fa3 100644 --- a/src/main/server/routerManager.ts +++ b/src/main/server/routerManager.ts @@ -8,7 +8,7 @@ import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis import path from 'path' import { dbPathDir } from 'apis/core/datastore/dbChecker' const STORE_PATH = dbPathDir() -const LOG_PATH = path.join(STORE_PATH, 'picgo.log') +const LOG_PATH = path.join(STORE_PATH, 'piclist.log') const errorMessage = `upload error. see ${LOG_PATH} for more detail.` @@ -22,9 +22,9 @@ router.post('/upload', async ({ try { if (list.length === 0) { // upload with clipboard - logger.info('[PicGo Server] upload clipboard file') + logger.info('[PicList Server] upload clipboard file') const res = await uploadClipboardFiles() - logger.info('[PicGo Server] upload result:', res) + logger.info('[PicList Server] upload result:', res) if (res) { handleResponse({ response, @@ -43,7 +43,7 @@ router.post('/upload', async ({ }) } } else { - logger.info('[PicGo Server] upload files in list') + logger.info('[PicList Server] upload files in list') // upload with files const pathList = list.map(item => { return { @@ -52,7 +52,7 @@ router.post('/upload', async ({ }) const win = windowManager.getAvailableWindow() const res = await uploadChoosedFiles(win.webContents, pathList) - logger.info('[PicGo Server] upload result', res.join(' ; ')) + logger.info('[PicList Server] upload result', res.join(' ; ')) if (res.length) { handleResponse({ response, diff --git a/src/main/server/utils.ts b/src/main/server/utils.ts index 7adf4c1..ba6c4e7 100644 --- a/src/main/server/utils.ts +++ b/src/main/server/utils.ts @@ -19,7 +19,7 @@ export const handleResponse = ({ body?: any }) => { if (body?.success === false) { - logger.warn('[PicGo Server] upload failed, see picgo.log for more detail ↑') + logger.warn('[PicList Server] upload failed, see piclist.log for more detail ↑') } response.writeHead(statusCode, header) response.write(JSON.stringify(body)) diff --git a/src/main/utils/beforeOpen.ts b/src/main/utils/beforeOpen.ts index fd1a807..e124c4a 100644 --- a/src/main/utils/beforeOpen.ts +++ b/src/main/utils/beforeOpen.ts @@ -4,7 +4,6 @@ import os from 'os' import { dbPathChecker } from 'apis/core/datastore/dbChecker' import yaml from 'js-yaml' import { i18nManager } from '~/main/i18n' -// import { ILocales } from '~/universal/types/i18n' const configPath = dbPathChecker() const CONFIG_DIR = path.dirname(configPath) @@ -21,12 +20,12 @@ function beforeOpen () { * macOS 右键菜单 */ function resolveMacWorkFlow () { - const dest = `${os.homedir()}/Library/Services/Upload pictures with PicGo.workflow` + const dest = `${os.homedir()}/Library/Services/Upload pictures with PicList.workflow` if (fs.existsSync(dest)) { return true } else { try { - fs.copySync(path.join(__static, 'Upload pictures with PicGo.workflow'), dest) + fs.copySync(path.join(__static, 'Upload pictures with PicList.workflow'), dest) } catch (e) { console.log(e) } diff --git a/src/main/utils/handleArgv.ts b/src/main/utils/handleArgv.ts index 39f1517..3e80c1f 100644 --- a/src/main/utils/handleArgv.ts +++ b/src/main/utils/handleArgv.ts @@ -1,6 +1,6 @@ import path from 'path' import fs from 'fs-extra' -import { Logger } from 'picgo' +import { Logger } from 'piclist' import { isUrl } from '~/universal/utils/common' interface IResultFileObject { path: string diff --git a/src/main/utils/updateChecker.ts b/src/main/utils/updateChecker.ts index 85c6d97..eb7ca24 100644 --- a/src/main/utils/updateChecker.ts +++ b/src/main/utils/updateChecker.ts @@ -7,7 +7,7 @@ import { getLatestVersion } from '#/utils/getLatestVersion' const version = pkg.version // const releaseUrl = 'https://api.github.com/repos/Molunerfinn/PicGo/releases' // const releaseUrlBackup = 'https://picgo-1251750343.cos.ap-chengdu.myqcloud.com' -const downloadUrl = 'https://github.com/Molunerfinn/PicGo/releases/latest' +const downloadUrl = 'https://github.com/Kuingsmile/PicList/releases/latest' const checkVersion = async () => { let showTip = db.get('settings.showUpdateTip') @@ -16,8 +16,7 @@ const checkVersion = async () => { showTip = true } if (showTip) { - const isCheckBetaUpdate = db.get('settings.checkBetaUpdate') !== false - const res: string = await getLatestVersion(isCheckBetaUpdate) + const res: string = await getLatestVersion() if (res !== '') { const latest = res const result = compareVersion2Update(version, latest) @@ -49,12 +48,6 @@ const checkVersion = async () => { // if true -> update else return false const compareVersion2Update = (current: string, latest: string) => { try { - if (latest.includes('beta')) { - const isCheckBetaUpdate = db.get('settings.checkBetaUpdate') !== false - if (!isCheckBetaUpdate) { - return false - } - } return lt(current, latest) } catch (e) { return false diff --git a/src/renderer/App.vue b/src/renderer/App.vue index ef17ed0..b7b3799 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -8,7 +8,7 @@ import { useStore } from '@/hooks/useStore' import { onBeforeMount, onMounted, onUnmounted } from 'vue' import { getConfig } from './utils/dataSender' -import type { IConfig } from 'picgo' +import type { IConfig } from 'piclist' import bus from './utils/bus' import { FORCE_UPDATE } from '~/universal/events/constants' diff --git a/src/renderer/apis/aliyun.ts b/src/renderer/apis/aliyun.ts new file mode 100644 index 0000000..dba2acb --- /dev/null +++ b/src/renderer/apis/aliyun.ts @@ -0,0 +1,25 @@ +import OSS from 'ali-oss' + +export default class AliyunApi { + static async delete (configMap: IStringKeyMap): Promise { + const { fileName, config: { accessKeyId, accessKeySecret, bucket, area, path } } = configMap + try { + const client = new OSS({ + accessKeyId, + accessKeySecret, + bucket, + region: area + }) + let key + if (path === '/' || !path) { + key = fileName + } else { + key = `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}` + } + const result = await client.delete(key) as any + return result.res.status === 204 + } catch (error) { + return false + } + } +} diff --git a/src/renderer/apis/allApi.ts b/src/renderer/apis/allApi.ts new file mode 100644 index 0000000..8f7a970 --- /dev/null +++ b/src/renderer/apis/allApi.ts @@ -0,0 +1,27 @@ +import SmmsApi from './smms' +import TcyunApi from './tcyun' +import AliyunApi from './aliyun' +import QiniuApi from './qiniu' +import ImgurApi from './imgur' +import GithubApi from './github' +import UpyunApi from './upyun' + +const apiMap: IStringKeyMap = { + smms: SmmsApi, + tcyun: TcyunApi, + aliyun: AliyunApi, + qiniu: QiniuApi, + imgur: ImgurApi, + github: GithubApi, + upyun: UpyunApi +} + +export default class ALLApi { + static async delete (configMap: IStringKeyMap): Promise { + if (apiMap[configMap.type] !== undefined) { + return await apiMap[configMap.type].delete(configMap) + } else { + return false + } + } +} diff --git a/src/renderer/apis/github.ts b/src/renderer/apis/github.ts new file mode 100644 index 0000000..f98ff1a --- /dev/null +++ b/src/renderer/apis/github.ts @@ -0,0 +1,31 @@ +import { Octokit } from '@octokit/rest' + +export default class GithubApi { + static async delete (configMap: IStringKeyMap): Promise { + const { fileName, hash, config: { repo, token, branch, path } } = configMap + const owner = repo.split('/')[0] + const repoName = repo.split('/')[1] + const octokit = new Octokit({ + auth: token + }) + let key + if (path === '/' || !path) { + key = fileName + } else { + key = `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}` + } + try { + const result = await octokit.rest.repos.deleteFile({ + owner, + repo: repoName, + path: key, + message: `delete ${fileName} by PicList`, + sha: hash, + branch + }) + return result.status === 200 + } catch (error) { + return false + } + } +} diff --git a/src/renderer/apis/imgur.ts b/src/renderer/apis/imgur.ts new file mode 100644 index 0000000..7cdc46f --- /dev/null +++ b/src/renderer/apis/imgur.ts @@ -0,0 +1,21 @@ +import axios from 'axios' + +export default class ImgurApi { + static async delete (configMap: IStringKeyMap): Promise { + const clientId = configMap.config.clientId + const { hash } = configMap + const fullUrl = `https://api.imgur.com/3/image/${hash}` + const headers = { + Authorization: `Client-ID ${clientId}` + } + try { + const res = await axios.delete(fullUrl, { + headers, + timeout: 10000 + }) + return res.status === 200 + } catch (error) { + return false + } + } +} diff --git a/src/renderer/apis/qiniu.ts b/src/renderer/apis/qiniu.ts new file mode 100644 index 0000000..5ad5980 --- /dev/null +++ b/src/renderer/apis/qiniu.ts @@ -0,0 +1,33 @@ +import Qiniu from 'qiniu' + +export default class QiniuApi { + static async delete (configMap: IStringKeyMap): Promise { + const { fileName, config: { accessKey, secretKey, bucket, path } } = configMap + const mac = new Qiniu.auth.digest.Mac(accessKey, secretKey) + const qiniuConfig = new Qiniu.conf.Config() + try { + const bucketManager = new Qiniu.rs.BucketManager(mac, qiniuConfig) + let key = '' + if (path === '/' || !path) { + key = fileName + } else { + key = `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}` + } + const res = await new Promise((resolve, reject) => { + bucketManager.delete(bucket, key, (err, respBody, respInfo) => { + if (err) { + reject(err) + } else { + resolve({ + respBody, + respInfo + }) + } + }) + }) as any + return res && res.respInfo.statusCode === 200 + } catch (error) { + return false + } + } +} diff --git a/src/renderer/apis/smms.ts b/src/renderer/apis/smms.ts new file mode 100644 index 0000000..c159095 --- /dev/null +++ b/src/renderer/apis/smms.ts @@ -0,0 +1,23 @@ +import axios from 'axios' + +export default class SmmsApi { + static async delete (configMap: IStringKeyMap): Promise { + const { hash, config: { token } } = configMap + if (!hash || !token) { + return false + } else { + const res = await axios.get( + `https://smms.app/api/v2/delete/${hash}`, { + headers: { + Authorization: token + }, + params: { + hash, + format: 'json' + }, + timeout: 10000 + }) + return res.status === 200 + } + } +} diff --git a/src/renderer/apis/tcyun.ts b/src/renderer/apis/tcyun.ts new file mode 100644 index 0000000..32eb150 --- /dev/null +++ b/src/renderer/apis/tcyun.ts @@ -0,0 +1,27 @@ +import COS from 'cos-nodejs-sdk-v5' + +export default class TcyunApi { + static async delete (configMap: IStringKeyMap): Promise { + const { fileName, config: { secretId, secretKey, bucket, area, path } } = configMap + try { + const cos = new COS({ + SecretId: secretId, + SecretKey: secretKey + }) + let key + if (path === '/' || !path) { + key = `/${fileName}` + } else { + key = `/${path.replace(/^\//, '').replace(/\/$/, '')}${fileName}` + } + const result = await cos.deleteObject({ + Bucket: bucket, + Region: area, + Key: key + }) + return result.statusCode === 204 + } catch (error) { + return false + } + } +} diff --git a/src/renderer/apis/upyun.ts b/src/renderer/apis/upyun.ts new file mode 100644 index 0000000..5a1d070 --- /dev/null +++ b/src/renderer/apis/upyun.ts @@ -0,0 +1,22 @@ +// @ts-ignore +import Upyun from 'upyun' + +export default class UpyunApi { + static async delete (configMap: IStringKeyMap): Promise { + const { fileName, config: { bucket, operator, password, path } } = configMap + try { + const service = new Upyun.Service(bucket, operator, password) + const client = new Upyun.Client(service) + let key + if (path === '/' || !path) { + key = fileName + } else { + key = `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}` + } + const result = await client.deleteFile(key) + return result + } catch (error) { + return false + } + } +} diff --git a/src/renderer/layouts/Main.vue b/src/renderer/layouts/Main.vue index 6591648..64b3b5b 100644 --- a/src/renderer/layouts/Main.vue +++ b/src/renderer/layouts/Main.vue @@ -51,11 +51,11 @@ {{ $T('UPLOAD_AREA') }} - + - + - {{ $T('PICBEDS_MANAGE') }} + 管理页面 @@ -105,8 +105,8 @@ - {{ $T('PICGO_SPONSOR_TEXT') }} + {{ $T('PICLIST_SPONSOR_TEXT') }} { type } }) - // if (this.$builtInPicBed.includes(picBed)) { - // this.$router.push({ - // name: picBed - // }) - // } else { - // this.$router.push({ - // name: 'others', - // params: { - // type: picBed - // } - // }) - // } } } @@ -332,7 +321,7 @@ function openMiniWindow () { function handleCopyPicBedConfig () { clipboard.writeText(picBedConfigString.value) - $message.success(T('COPY_PICBED_CONFIG_SUCCEED')) + $message.success($T('COPY_PICBED_CONFIG_SUCCEED')) } function getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) { diff --git a/src/renderer/manage/ManageMain.vue b/src/renderer/manage/ManageMain.vue deleted file mode 100644 index c3bfe47..0000000 --- a/src/renderer/manage/ManageMain.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/src/renderer/manage/pages/assets/aliyun.png b/src/renderer/manage/pages/assets/aliyun.png new file mode 100644 index 0000000..44b9f47 Binary files /dev/null and b/src/renderer/manage/pages/assets/aliyun.png differ diff --git a/src/renderer/manage/pages/assets/github.png b/src/renderer/manage/pages/assets/github.png new file mode 100644 index 0000000..10737d4 Binary files /dev/null and b/src/renderer/manage/pages/assets/github.png differ diff --git a/src/renderer/manage/pages/assets/icons/3g2.png b/src/renderer/manage/pages/assets/icons/3g2.png new file mode 100644 index 0000000..2fa0612 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/3g2.png differ diff --git a/src/renderer/manage/pages/assets/icons/3gp.png b/src/renderer/manage/pages/assets/icons/3gp.png new file mode 100644 index 0000000..2df8ba0 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/3gp.png differ diff --git a/src/renderer/manage/pages/assets/icons/7z.png b/src/renderer/manage/pages/assets/icons/7z.png new file mode 100644 index 0000000..35f4994 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/7z.png differ diff --git a/src/renderer/manage/pages/assets/icons/_blank.png b/src/renderer/manage/pages/assets/icons/_blank.png new file mode 100644 index 0000000..ae53a4e Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/_blank.png differ diff --git a/src/renderer/manage/pages/assets/icons/_page.png b/src/renderer/manage/pages/assets/icons/_page.png new file mode 100644 index 0000000..b8d155e Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/_page.png differ diff --git a/src/renderer/manage/pages/assets/icons/aac.png b/src/renderer/manage/pages/assets/icons/aac.png new file mode 100644 index 0000000..200db51 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/aac.png differ diff --git a/src/renderer/manage/pages/assets/icons/accdb.png b/src/renderer/manage/pages/assets/icons/accdb.png new file mode 100644 index 0000000..347d4d0 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/accdb.png differ diff --git a/src/renderer/manage/pages/assets/icons/adt.png b/src/renderer/manage/pages/assets/icons/adt.png new file mode 100644 index 0000000..1d8f90d Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/adt.png differ diff --git a/src/renderer/manage/pages/assets/icons/ai.png b/src/renderer/manage/pages/assets/icons/ai.png new file mode 100644 index 0000000..c1810f5 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ai.png differ diff --git a/src/renderer/manage/pages/assets/icons/aiff.png b/src/renderer/manage/pages/assets/icons/aiff.png new file mode 100644 index 0000000..f9f1faf Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/aiff.png differ diff --git a/src/renderer/manage/pages/assets/icons/aly.png b/src/renderer/manage/pages/assets/icons/aly.png new file mode 100644 index 0000000..c7b0ce0 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/aly.png differ diff --git a/src/renderer/manage/pages/assets/icons/amiga.png b/src/renderer/manage/pages/assets/icons/amiga.png new file mode 100644 index 0000000..9dab7ca Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/amiga.png differ diff --git a/src/renderer/manage/pages/assets/icons/amr.png b/src/renderer/manage/pages/assets/icons/amr.png new file mode 100644 index 0000000..141dfef Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/amr.png differ diff --git a/src/renderer/manage/pages/assets/icons/ape.png b/src/renderer/manage/pages/assets/icons/ape.png new file mode 100644 index 0000000..8eec427 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ape.png differ diff --git a/src/renderer/manage/pages/assets/icons/apk.png b/src/renderer/manage/pages/assets/icons/apk.png new file mode 100644 index 0000000..3162c45 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/apk.png differ diff --git a/src/renderer/manage/pages/assets/icons/arj.png b/src/renderer/manage/pages/assets/icons/arj.png new file mode 100644 index 0000000..8d200b2 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/arj.png differ diff --git a/src/renderer/manage/pages/assets/icons/asf.png b/src/renderer/manage/pages/assets/icons/asf.png new file mode 100644 index 0000000..936ead5 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/asf.png differ diff --git a/src/renderer/manage/pages/assets/icons/asm.png b/src/renderer/manage/pages/assets/icons/asm.png new file mode 100644 index 0000000..79f28b9 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/asm.png differ diff --git a/src/renderer/manage/pages/assets/icons/asx.png b/src/renderer/manage/pages/assets/icons/asx.png new file mode 100644 index 0000000..f6eb4fc Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/asx.png differ diff --git a/src/renderer/manage/pages/assets/icons/au.png b/src/renderer/manage/pages/assets/icons/au.png new file mode 100644 index 0000000..1d33a36 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/au.png differ diff --git a/src/renderer/manage/pages/assets/icons/avc.png b/src/renderer/manage/pages/assets/icons/avc.png new file mode 100644 index 0000000..85e4d93 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/avc.png differ diff --git a/src/renderer/manage/pages/assets/icons/avi.png b/src/renderer/manage/pages/assets/icons/avi.png new file mode 100644 index 0000000..6ddfcea Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/avi.png differ diff --git a/src/renderer/manage/pages/assets/icons/avs.png b/src/renderer/manage/pages/assets/icons/avs.png new file mode 100644 index 0000000..c5796c9 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/avs.png differ diff --git a/src/renderer/manage/pages/assets/icons/bak.png b/src/renderer/manage/pages/assets/icons/bak.png new file mode 100644 index 0000000..bb94fe3 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/bak.png differ diff --git a/src/renderer/manage/pages/assets/icons/bas.png b/src/renderer/manage/pages/assets/icons/bas.png new file mode 100644 index 0000000..027d5fe Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/bas.png differ diff --git a/src/renderer/manage/pages/assets/icons/bat.png b/src/renderer/manage/pages/assets/icons/bat.png new file mode 100644 index 0000000..3b32532 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/bat.png differ diff --git a/src/renderer/manage/pages/assets/icons/bmp.png b/src/renderer/manage/pages/assets/icons/bmp.png new file mode 100644 index 0000000..0efcc32 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/bmp.png differ diff --git a/src/renderer/manage/pages/assets/icons/bom.png b/src/renderer/manage/pages/assets/icons/bom.png new file mode 100644 index 0000000..7098165 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/bom.png differ diff --git a/src/renderer/manage/pages/assets/icons/c.png b/src/renderer/manage/pages/assets/icons/c.png new file mode 100644 index 0000000..249e6c7 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/c.png differ diff --git a/src/renderer/manage/pages/assets/icons/cda.png b/src/renderer/manage/pages/assets/icons/cda.png new file mode 100644 index 0000000..5dd192f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/cda.png differ diff --git a/src/renderer/manage/pages/assets/icons/cdr.png b/src/renderer/manage/pages/assets/icons/cdr.png new file mode 100644 index 0000000..fbc728b Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/cdr.png differ diff --git a/src/renderer/manage/pages/assets/icons/chm.png b/src/renderer/manage/pages/assets/icons/chm.png new file mode 100644 index 0000000..94bb993 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/chm.png differ diff --git a/src/renderer/manage/pages/assets/icons/class.png b/src/renderer/manage/pages/assets/icons/class.png new file mode 100644 index 0000000..13429d1 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/class.png differ diff --git a/src/renderer/manage/pages/assets/icons/cmd.png b/src/renderer/manage/pages/assets/icons/cmd.png new file mode 100644 index 0000000..427f086 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/cmd.png differ diff --git a/src/renderer/manage/pages/assets/icons/com.png b/src/renderer/manage/pages/assets/icons/com.png new file mode 100644 index 0000000..39c7b46 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/com.png differ diff --git a/src/renderer/manage/pages/assets/icons/cpp.png b/src/renderer/manage/pages/assets/icons/cpp.png new file mode 100644 index 0000000..0a6cd8b Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/cpp.png differ diff --git a/src/renderer/manage/pages/assets/icons/css.png b/src/renderer/manage/pages/assets/icons/css.png new file mode 100644 index 0000000..8035b9d Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/css.png differ diff --git a/src/renderer/manage/pages/assets/icons/csv.png b/src/renderer/manage/pages/assets/icons/csv.png new file mode 100644 index 0000000..05a08dc Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/csv.png differ diff --git a/src/renderer/manage/pages/assets/icons/dart.png b/src/renderer/manage/pages/assets/icons/dart.png new file mode 100644 index 0000000..47e4b7b Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dart.png differ diff --git a/src/renderer/manage/pages/assets/icons/dat.png b/src/renderer/manage/pages/assets/icons/dat.png new file mode 100644 index 0000000..971e364 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dat.png differ diff --git a/src/renderer/manage/pages/assets/icons/ddb.png b/src/renderer/manage/pages/assets/icons/ddb.png new file mode 100644 index 0000000..0fcbb1c Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ddb.png differ diff --git a/src/renderer/manage/pages/assets/icons/dif.png b/src/renderer/manage/pages/assets/icons/dif.png new file mode 100644 index 0000000..61c3dba Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dif.png differ diff --git a/src/renderer/manage/pages/assets/icons/divx.png b/src/renderer/manage/pages/assets/icons/divx.png new file mode 100644 index 0000000..54b7c10 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/divx.png differ diff --git a/src/renderer/manage/pages/assets/icons/dll.png b/src/renderer/manage/pages/assets/icons/dll.png new file mode 100644 index 0000000..140e1f3 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dll.png differ diff --git a/src/renderer/manage/pages/assets/icons/dmg.png b/src/renderer/manage/pages/assets/icons/dmg.png new file mode 100644 index 0000000..94a38ae Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dmg.png differ diff --git a/src/renderer/manage/pages/assets/icons/doc.png b/src/renderer/manage/pages/assets/icons/doc.png new file mode 100644 index 0000000..aff8234 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/doc.png differ diff --git a/src/renderer/manage/pages/assets/icons/docm.png b/src/renderer/manage/pages/assets/icons/docm.png new file mode 100644 index 0000000..8fffaa8 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/docm.png differ diff --git a/src/renderer/manage/pages/assets/icons/docx.png b/src/renderer/manage/pages/assets/icons/docx.png new file mode 100644 index 0000000..6824077 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/docx.png differ diff --git a/src/renderer/manage/pages/assets/icons/dot.png b/src/renderer/manage/pages/assets/icons/dot.png new file mode 100644 index 0000000..5052be4 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dot.png differ diff --git a/src/renderer/manage/pages/assets/icons/dotm.png b/src/renderer/manage/pages/assets/icons/dotm.png new file mode 100644 index 0000000..eedf78d Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dotm.png differ diff --git a/src/renderer/manage/pages/assets/icons/dotx.png b/src/renderer/manage/pages/assets/icons/dotx.png new file mode 100644 index 0000000..b0220e3 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dotx.png differ diff --git a/src/renderer/manage/pages/assets/icons/dsl.png b/src/renderer/manage/pages/assets/icons/dsl.png new file mode 100644 index 0000000..4214ae0 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dsl.png differ diff --git a/src/renderer/manage/pages/assets/icons/dv.png b/src/renderer/manage/pages/assets/icons/dv.png new file mode 100644 index 0000000..1ab0c31 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dv.png differ diff --git a/src/renderer/manage/pages/assets/icons/dvd.png b/src/renderer/manage/pages/assets/icons/dvd.png new file mode 100644 index 0000000..1e4e08f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dvd.png differ diff --git a/src/renderer/manage/pages/assets/icons/dvdaudio.png b/src/renderer/manage/pages/assets/icons/dvdaudio.png new file mode 100644 index 0000000..3f34ca0 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dvdaudio.png differ diff --git a/src/renderer/manage/pages/assets/icons/dwg.png b/src/renderer/manage/pages/assets/icons/dwg.png new file mode 100644 index 0000000..de4fe53 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dwg.png differ diff --git a/src/renderer/manage/pages/assets/icons/dxf.png b/src/renderer/manage/pages/assets/icons/dxf.png new file mode 100644 index 0000000..2ed2465 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/dxf.png differ diff --git a/src/renderer/manage/pages/assets/icons/emf.png b/src/renderer/manage/pages/assets/icons/emf.png new file mode 100644 index 0000000..c8397b7 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/emf.png differ diff --git a/src/renderer/manage/pages/assets/icons/env.png b/src/renderer/manage/pages/assets/icons/env.png new file mode 100644 index 0000000..4dda6b1 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/env.png differ diff --git a/src/renderer/manage/pages/assets/icons/eot.png b/src/renderer/manage/pages/assets/icons/eot.png new file mode 100644 index 0000000..9dcce58 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/eot.png differ diff --git a/src/renderer/manage/pages/assets/icons/eps.png b/src/renderer/manage/pages/assets/icons/eps.png new file mode 100644 index 0000000..cc32db8 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/eps.png differ diff --git a/src/renderer/manage/pages/assets/icons/exe.png b/src/renderer/manage/pages/assets/icons/exe.png new file mode 100644 index 0000000..60e5f6b Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/exe.png differ diff --git a/src/renderer/manage/pages/assets/icons/exif.png b/src/renderer/manage/pages/assets/icons/exif.png new file mode 100644 index 0000000..81f3096 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/exif.png differ diff --git a/src/renderer/manage/pages/assets/icons/fakesmms.png b/src/renderer/manage/pages/assets/icons/fakesmms.png new file mode 100644 index 0000000..4631b7e Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/fakesmms.png differ diff --git a/src/renderer/manage/pages/assets/icons/flc.png b/src/renderer/manage/pages/assets/icons/flc.png new file mode 100644 index 0000000..0c97f9f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/flc.png differ diff --git a/src/renderer/manage/pages/assets/icons/fli.png b/src/renderer/manage/pages/assets/icons/fli.png new file mode 100644 index 0000000..0dd55bd Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/fli.png differ diff --git a/src/renderer/manage/pages/assets/icons/flv.png b/src/renderer/manage/pages/assets/icons/flv.png new file mode 100644 index 0000000..73bc4cc Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/flv.png differ diff --git a/src/renderer/manage/pages/assets/icons/folder.png b/src/renderer/manage/pages/assets/icons/folder.png new file mode 100644 index 0000000..9d97671 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/folder.png differ diff --git a/src/renderer/manage/pages/assets/icons/fon.png b/src/renderer/manage/pages/assets/icons/fon.png new file mode 100644 index 0000000..7ebb73c Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/fon.png differ diff --git a/src/renderer/manage/pages/assets/icons/font.png b/src/renderer/manage/pages/assets/icons/font.png new file mode 100644 index 0000000..5775948 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/font.png differ diff --git a/src/renderer/manage/pages/assets/icons/for.png b/src/renderer/manage/pages/assets/icons/for.png new file mode 100644 index 0000000..d8ab231 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/for.png differ diff --git a/src/renderer/manage/pages/assets/icons/fpx.png b/src/renderer/manage/pages/assets/icons/fpx.png new file mode 100644 index 0000000..676ff8c Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/fpx.png differ diff --git a/src/renderer/manage/pages/assets/icons/fv.png b/src/renderer/manage/pages/assets/icons/fv.png new file mode 100644 index 0000000..d1db743 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/fv.png differ diff --git a/src/renderer/manage/pages/assets/icons/gif.png b/src/renderer/manage/pages/assets/icons/gif.png new file mode 100644 index 0000000..07e601f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/gif.png differ diff --git a/src/renderer/manage/pages/assets/icons/gitingore.png b/src/renderer/manage/pages/assets/icons/gitingore.png new file mode 100644 index 0000000..783c072 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/gitingore.png differ diff --git a/src/renderer/manage/pages/assets/icons/gitkeep.png b/src/renderer/manage/pages/assets/icons/gitkeep.png new file mode 100644 index 0000000..38cb5c0 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/gitkeep.png differ diff --git a/src/renderer/manage/pages/assets/icons/gz.png b/src/renderer/manage/pages/assets/icons/gz.png new file mode 100644 index 0000000..8cf9d75 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/gz.png differ diff --git a/src/renderer/manage/pages/assets/icons/h.png b/src/renderer/manage/pages/assets/icons/h.png new file mode 100644 index 0000000..f5cc5e5 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/h.png differ diff --git a/src/renderer/manage/pages/assets/icons/hdri.png b/src/renderer/manage/pages/assets/icons/hdri.png new file mode 100644 index 0000000..00fa619 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/hdri.png differ diff --git a/src/renderer/manage/pages/assets/icons/hlp.png b/src/renderer/manage/pages/assets/icons/hlp.png new file mode 100644 index 0000000..441d3f9 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/hlp.png differ diff --git a/src/renderer/manage/pages/assets/icons/hpp.png b/src/renderer/manage/pages/assets/icons/hpp.png new file mode 100644 index 0000000..ddcc8bf Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/hpp.png differ diff --git a/src/renderer/manage/pages/assets/icons/htm.png b/src/renderer/manage/pages/assets/icons/htm.png new file mode 100644 index 0000000..5bdfb06 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/htm.png differ diff --git a/src/renderer/manage/pages/assets/icons/html.png b/src/renderer/manage/pages/assets/icons/html.png new file mode 100644 index 0000000..454cd9f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/html.png differ diff --git a/src/renderer/manage/pages/assets/icons/ico.png b/src/renderer/manage/pages/assets/icons/ico.png new file mode 100644 index 0000000..f029bab Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ico.png differ diff --git a/src/renderer/manage/pages/assets/icons/ics.png b/src/renderer/manage/pages/assets/icons/ics.png new file mode 100644 index 0000000..7a0f5c0 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ics.png differ diff --git a/src/renderer/manage/pages/assets/icons/int.png b/src/renderer/manage/pages/assets/icons/int.png new file mode 100644 index 0000000..30f0329 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/int.png differ diff --git a/src/renderer/manage/pages/assets/icons/ipynb.png b/src/renderer/manage/pages/assets/icons/ipynb.png new file mode 100644 index 0000000..a2778b3 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ipynb.png differ diff --git a/src/renderer/manage/pages/assets/icons/iso.png b/src/renderer/manage/pages/assets/icons/iso.png new file mode 100644 index 0000000..729fa7f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/iso.png differ diff --git a/src/renderer/manage/pages/assets/icons/java.png b/src/renderer/manage/pages/assets/icons/java.png new file mode 100644 index 0000000..0d46a4a Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/java.png differ diff --git a/src/renderer/manage/pages/assets/icons/jpeg.png b/src/renderer/manage/pages/assets/icons/jpeg.png new file mode 100644 index 0000000..4262c4e Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/jpeg.png differ diff --git a/src/renderer/manage/pages/assets/icons/jpg.png b/src/renderer/manage/pages/assets/icons/jpg.png new file mode 100644 index 0000000..4262c4e Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/jpg.png differ diff --git a/src/renderer/manage/pages/assets/icons/js.png b/src/renderer/manage/pages/assets/icons/js.png new file mode 100644 index 0000000..507661c Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/js.png differ diff --git a/src/renderer/manage/pages/assets/icons/json.png b/src/renderer/manage/pages/assets/icons/json.png new file mode 100644 index 0000000..362ef40 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/json.png differ diff --git a/src/renderer/manage/pages/assets/icons/key.png b/src/renderer/manage/pages/assets/icons/key.png new file mode 100644 index 0000000..44ab47e Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/key.png differ diff --git a/src/renderer/manage/pages/assets/icons/ksp.png b/src/renderer/manage/pages/assets/icons/ksp.png new file mode 100644 index 0000000..a76fb10 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ksp.png differ diff --git a/src/renderer/manage/pages/assets/icons/less.png b/src/renderer/manage/pages/assets/icons/less.png new file mode 100644 index 0000000..2d7b56f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/less.png differ diff --git a/src/renderer/manage/pages/assets/icons/lib.png b/src/renderer/manage/pages/assets/icons/lib.png new file mode 100644 index 0000000..390082a Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/lib.png differ diff --git a/src/renderer/manage/pages/assets/icons/lic.png b/src/renderer/manage/pages/assets/icons/lic.png new file mode 100644 index 0000000..bd28dbc Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/lic.png differ diff --git a/src/renderer/manage/pages/assets/icons/license.png b/src/renderer/manage/pages/assets/icons/license.png new file mode 100644 index 0000000..0f5b2f1 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/license.png differ diff --git a/src/renderer/manage/pages/assets/icons/log.png b/src/renderer/manage/pages/assets/icons/log.png new file mode 100644 index 0000000..7899d19 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/log.png differ diff --git a/src/renderer/manage/pages/assets/icons/lst.png b/src/renderer/manage/pages/assets/icons/lst.png new file mode 100644 index 0000000..a74d27d Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/lst.png differ diff --git a/src/renderer/manage/pages/assets/icons/lua.png b/src/renderer/manage/pages/assets/icons/lua.png new file mode 100644 index 0000000..f7d2b67 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/lua.png differ diff --git a/src/renderer/manage/pages/assets/icons/mac.png b/src/renderer/manage/pages/assets/icons/mac.png new file mode 100644 index 0000000..776f6fd Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mac.png differ diff --git a/src/renderer/manage/pages/assets/icons/map.png b/src/renderer/manage/pages/assets/icons/map.png new file mode 100644 index 0000000..88e944b Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/map.png differ diff --git a/src/renderer/manage/pages/assets/icons/markdown.png b/src/renderer/manage/pages/assets/icons/markdown.png new file mode 100644 index 0000000..6122564 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/markdown.png differ diff --git a/src/renderer/manage/pages/assets/icons/md.png b/src/renderer/manage/pages/assets/icons/md.png new file mode 100644 index 0000000..6122564 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/md.png differ diff --git a/src/renderer/manage/pages/assets/icons/mdf.png b/src/renderer/manage/pages/assets/icons/mdf.png new file mode 100644 index 0000000..81bf563 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mdf.png differ diff --git a/src/renderer/manage/pages/assets/icons/mht.png b/src/renderer/manage/pages/assets/icons/mht.png new file mode 100644 index 0000000..dadaf7a Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mht.png differ diff --git a/src/renderer/manage/pages/assets/icons/mhtml.png b/src/renderer/manage/pages/assets/icons/mhtml.png new file mode 100644 index 0000000..5b0be8f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mhtml.png differ diff --git a/src/renderer/manage/pages/assets/icons/mid.png b/src/renderer/manage/pages/assets/icons/mid.png new file mode 100644 index 0000000..fc50598 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mid.png differ diff --git a/src/renderer/manage/pages/assets/icons/midi.png b/src/renderer/manage/pages/assets/icons/midi.png new file mode 100644 index 0000000..a32159f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/midi.png differ diff --git a/src/renderer/manage/pages/assets/icons/mkv.png b/src/renderer/manage/pages/assets/icons/mkv.png new file mode 100644 index 0000000..327a6f1 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mkv.png differ diff --git a/src/renderer/manage/pages/assets/icons/mmf.png b/src/renderer/manage/pages/assets/icons/mmf.png new file mode 100644 index 0000000..3ebedde Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mmf.png differ diff --git a/src/renderer/manage/pages/assets/icons/mod.png b/src/renderer/manage/pages/assets/icons/mod.png new file mode 100644 index 0000000..89ec567 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mod.png differ diff --git a/src/renderer/manage/pages/assets/icons/mov.png b/src/renderer/manage/pages/assets/icons/mov.png new file mode 100644 index 0000000..03f2350 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mov.png differ diff --git a/src/renderer/manage/pages/assets/icons/mp2.png b/src/renderer/manage/pages/assets/icons/mp2.png new file mode 100644 index 0000000..7e4a87a Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mp2.png differ diff --git a/src/renderer/manage/pages/assets/icons/mp3.png b/src/renderer/manage/pages/assets/icons/mp3.png new file mode 100644 index 0000000..568a51c Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mp3.png differ diff --git a/src/renderer/manage/pages/assets/icons/mp4.png b/src/renderer/manage/pages/assets/icons/mp4.png new file mode 100644 index 0000000..c83c923 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mp4.png differ diff --git a/src/renderer/manage/pages/assets/icons/mpa.png b/src/renderer/manage/pages/assets/icons/mpa.png new file mode 100644 index 0000000..de4a45f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mpa.png differ diff --git a/src/renderer/manage/pages/assets/icons/mpe.png b/src/renderer/manage/pages/assets/icons/mpe.png new file mode 100644 index 0000000..42fe795 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mpe.png differ diff --git a/src/renderer/manage/pages/assets/icons/mpeg.png b/src/renderer/manage/pages/assets/icons/mpeg.png new file mode 100644 index 0000000..c8eb903 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mpeg.png differ diff --git a/src/renderer/manage/pages/assets/icons/mpeg1.png b/src/renderer/manage/pages/assets/icons/mpeg1.png new file mode 100644 index 0000000..cccc2bc Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mpeg1.png differ diff --git a/src/renderer/manage/pages/assets/icons/mpeg2.png b/src/renderer/manage/pages/assets/icons/mpeg2.png new file mode 100644 index 0000000..c2d4168 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mpeg2.png differ diff --git a/src/renderer/manage/pages/assets/icons/mpg.png b/src/renderer/manage/pages/assets/icons/mpg.png new file mode 100644 index 0000000..e106159 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mpg.png differ diff --git a/src/renderer/manage/pages/assets/icons/mppro.png b/src/renderer/manage/pages/assets/icons/mppro.png new file mode 100644 index 0000000..fee4469 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mppro.png differ diff --git a/src/renderer/manage/pages/assets/icons/msg.png b/src/renderer/manage/pages/assets/icons/msg.png new file mode 100644 index 0000000..f4df81f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/msg.png differ diff --git a/src/renderer/manage/pages/assets/icons/mts.png b/src/renderer/manage/pages/assets/icons/mts.png new file mode 100644 index 0000000..43e11ad Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mts.png differ diff --git a/src/renderer/manage/pages/assets/icons/mux.png b/src/renderer/manage/pages/assets/icons/mux.png new file mode 100644 index 0000000..9cac3ee Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mux.png differ diff --git a/src/renderer/manage/pages/assets/icons/mv.png b/src/renderer/manage/pages/assets/icons/mv.png new file mode 100644 index 0000000..253170f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/mv.png differ diff --git a/src/renderer/manage/pages/assets/icons/navi.png b/src/renderer/manage/pages/assets/icons/navi.png new file mode 100644 index 0000000..1ec5e97 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/navi.png differ diff --git a/src/renderer/manage/pages/assets/icons/obj.png b/src/renderer/manage/pages/assets/icons/obj.png new file mode 100644 index 0000000..cd9bbf2 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/obj.png differ diff --git a/src/renderer/manage/pages/assets/icons/odf.png b/src/renderer/manage/pages/assets/icons/odf.png new file mode 100644 index 0000000..8173771 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/odf.png differ diff --git a/src/renderer/manage/pages/assets/icons/ods.png b/src/renderer/manage/pages/assets/icons/ods.png new file mode 100644 index 0000000..a3c5cc2 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ods.png differ diff --git a/src/renderer/manage/pages/assets/icons/odt.png b/src/renderer/manage/pages/assets/icons/odt.png new file mode 100644 index 0000000..1e72fd4 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/odt.png differ diff --git a/src/renderer/manage/pages/assets/icons/ogg.png b/src/renderer/manage/pages/assets/icons/ogg.png new file mode 100644 index 0000000..7d3a645 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ogg.png differ diff --git a/src/renderer/manage/pages/assets/icons/one.png b/src/renderer/manage/pages/assets/icons/one.png new file mode 100644 index 0000000..e32a037 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/one.png differ diff --git a/src/renderer/manage/pages/assets/icons/otf.png b/src/renderer/manage/pages/assets/icons/otf.png new file mode 100644 index 0000000..8722992 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/otf.png differ diff --git a/src/renderer/manage/pages/assets/icons/otp.png b/src/renderer/manage/pages/assets/icons/otp.png new file mode 100644 index 0000000..b419dc6 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/otp.png differ diff --git a/src/renderer/manage/pages/assets/icons/ots.png b/src/renderer/manage/pages/assets/icons/ots.png new file mode 100644 index 0000000..712b039 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ots.png differ diff --git a/src/renderer/manage/pages/assets/icons/ott.png b/src/renderer/manage/pages/assets/icons/ott.png new file mode 100644 index 0000000..2540efd Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ott.png differ diff --git a/src/renderer/manage/pages/assets/icons/pas.png b/src/renderer/manage/pages/assets/icons/pas.png new file mode 100644 index 0000000..180fb91 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/pas.png differ diff --git a/src/renderer/manage/pages/assets/icons/pcd.png b/src/renderer/manage/pages/assets/icons/pcd.png new file mode 100644 index 0000000..eb2bcbe Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/pcd.png differ diff --git a/src/renderer/manage/pages/assets/icons/pcx.png b/src/renderer/manage/pages/assets/icons/pcx.png new file mode 100644 index 0000000..34370f7 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/pcx.png differ diff --git a/src/renderer/manage/pages/assets/icons/pdf.png b/src/renderer/manage/pages/assets/icons/pdf.png new file mode 100644 index 0000000..b288c3f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/pdf.png differ diff --git a/src/renderer/manage/pages/assets/icons/php.png b/src/renderer/manage/pages/assets/icons/php.png new file mode 100644 index 0000000..615591e Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/php.png differ diff --git a/src/renderer/manage/pages/assets/icons/pic.png b/src/renderer/manage/pages/assets/icons/pic.png new file mode 100644 index 0000000..2e038f1 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/pic.png differ diff --git a/src/renderer/manage/pages/assets/icons/png.png b/src/renderer/manage/pages/assets/icons/png.png new file mode 100644 index 0000000..374333c Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/png.png differ diff --git a/src/renderer/manage/pages/assets/icons/ppt.png b/src/renderer/manage/pages/assets/icons/ppt.png new file mode 100644 index 0000000..42f8895 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ppt.png differ diff --git a/src/renderer/manage/pages/assets/icons/pptx.png b/src/renderer/manage/pages/assets/icons/pptx.png new file mode 100644 index 0000000..64e1438 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/pptx.png differ diff --git a/src/renderer/manage/pages/assets/icons/proe.png b/src/renderer/manage/pages/assets/icons/proe.png new file mode 100644 index 0000000..63ace85 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/proe.png differ diff --git a/src/renderer/manage/pages/assets/icons/prt.png b/src/renderer/manage/pages/assets/icons/prt.png new file mode 100644 index 0000000..7993b90 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/prt.png differ diff --git a/src/renderer/manage/pages/assets/icons/psd.png b/src/renderer/manage/pages/assets/icons/psd.png new file mode 100644 index 0000000..4351dd3 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/psd.png differ diff --git a/src/renderer/manage/pages/assets/icons/py.png b/src/renderer/manage/pages/assets/icons/py.png new file mode 100644 index 0000000..9e5668f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/py.png differ diff --git a/src/renderer/manage/pages/assets/icons/pyc.png b/src/renderer/manage/pages/assets/icons/pyc.png new file mode 100644 index 0000000..cbb7ce4 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/pyc.png differ diff --git a/src/renderer/manage/pages/assets/icons/qsv.png b/src/renderer/manage/pages/assets/icons/qsv.png new file mode 100644 index 0000000..88d4b20 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/qsv.png differ diff --git a/src/renderer/manage/pages/assets/icons/qt.png b/src/renderer/manage/pages/assets/icons/qt.png new file mode 100644 index 0000000..4deac47 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/qt.png differ diff --git a/src/renderer/manage/pages/assets/icons/quicktime.png b/src/renderer/manage/pages/assets/icons/quicktime.png new file mode 100644 index 0000000..b72491a Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/quicktime.png differ diff --git a/src/renderer/manage/pages/assets/icons/ra.png b/src/renderer/manage/pages/assets/icons/ra.png new file mode 100644 index 0000000..5994f56 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ra.png differ diff --git a/src/renderer/manage/pages/assets/icons/ram.png b/src/renderer/manage/pages/assets/icons/ram.png new file mode 100644 index 0000000..e314be0 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ram.png differ diff --git a/src/renderer/manage/pages/assets/icons/rar.png b/src/renderer/manage/pages/assets/icons/rar.png new file mode 100644 index 0000000..ad4b879 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/rar.png differ diff --git a/src/renderer/manage/pages/assets/icons/raw.png b/src/renderer/manage/pages/assets/icons/raw.png new file mode 100644 index 0000000..a6098c9 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/raw.png differ diff --git a/src/renderer/manage/pages/assets/icons/rb.png b/src/renderer/manage/pages/assets/icons/rb.png new file mode 100644 index 0000000..5b4a52b Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/rb.png differ diff --git a/src/renderer/manage/pages/assets/icons/realaudio.png b/src/renderer/manage/pages/assets/icons/realaudio.png new file mode 100644 index 0000000..6725dd1 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/realaudio.png differ diff --git a/src/renderer/manage/pages/assets/icons/rm.png b/src/renderer/manage/pages/assets/icons/rm.png new file mode 100644 index 0000000..e38cbe6 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/rm.png differ diff --git a/src/renderer/manage/pages/assets/icons/rmvb.png b/src/renderer/manage/pages/assets/icons/rmvb.png new file mode 100644 index 0000000..7cd97a6 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/rmvb.png differ diff --git a/src/renderer/manage/pages/assets/icons/rp.png b/src/renderer/manage/pages/assets/icons/rp.png new file mode 100644 index 0000000..9efccd8 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/rp.png differ diff --git a/src/renderer/manage/pages/assets/icons/rtf.png b/src/renderer/manage/pages/assets/icons/rtf.png new file mode 100644 index 0000000..99510b9 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/rtf.png differ diff --git a/src/renderer/manage/pages/assets/icons/s48.png b/src/renderer/manage/pages/assets/icons/s48.png new file mode 100644 index 0000000..a00dc24 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/s48.png differ diff --git a/src/renderer/manage/pages/assets/icons/sacd.png b/src/renderer/manage/pages/assets/icons/sacd.png new file mode 100644 index 0000000..f0d3351 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/sacd.png differ diff --git a/src/renderer/manage/pages/assets/icons/sass.png b/src/renderer/manage/pages/assets/icons/sass.png new file mode 100644 index 0000000..27ebf76 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/sass.png differ diff --git a/src/renderer/manage/pages/assets/icons/sch.png b/src/renderer/manage/pages/assets/icons/sch.png new file mode 100644 index 0000000..12ee11f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/sch.png differ diff --git a/src/renderer/manage/pages/assets/icons/scss.png b/src/renderer/manage/pages/assets/icons/scss.png new file mode 100644 index 0000000..33b47f4 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/scss.png differ diff --git a/src/renderer/manage/pages/assets/icons/sh.png b/src/renderer/manage/pages/assets/icons/sh.png new file mode 100644 index 0000000..47f99e3 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/sh.png differ diff --git a/src/renderer/manage/pages/assets/icons/sql.png b/src/renderer/manage/pages/assets/icons/sql.png new file mode 100644 index 0000000..1742560 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/sql.png differ diff --git a/src/renderer/manage/pages/assets/icons/stp.png b/src/renderer/manage/pages/assets/icons/stp.png new file mode 100644 index 0000000..9dea3bc Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/stp.png differ diff --git a/src/renderer/manage/pages/assets/icons/svcd.png b/src/renderer/manage/pages/assets/icons/svcd.png new file mode 100644 index 0000000..708c7ef Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/svcd.png differ diff --git a/src/renderer/manage/pages/assets/icons/svg.png b/src/renderer/manage/pages/assets/icons/svg.png new file mode 100644 index 0000000..ddc477d Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/svg.png differ diff --git a/src/renderer/manage/pages/assets/icons/swf.png b/src/renderer/manage/pages/assets/icons/swf.png new file mode 100644 index 0000000..02770be Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/swf.png differ diff --git a/src/renderer/manage/pages/assets/icons/sys.png b/src/renderer/manage/pages/assets/icons/sys.png new file mode 100644 index 0000000..a28488d Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/sys.png differ diff --git a/src/renderer/manage/pages/assets/icons/tga.png b/src/renderer/manage/pages/assets/icons/tga.png new file mode 100644 index 0000000..2a97d3d Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/tga.png differ diff --git a/src/renderer/manage/pages/assets/icons/tgz.png b/src/renderer/manage/pages/assets/icons/tgz.png new file mode 100644 index 0000000..2572e1f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/tgz.png differ diff --git a/src/renderer/manage/pages/assets/icons/tiff.png b/src/renderer/manage/pages/assets/icons/tiff.png new file mode 100644 index 0000000..a44d071 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/tiff.png differ diff --git a/src/renderer/manage/pages/assets/icons/tmp.png b/src/renderer/manage/pages/assets/icons/tmp.png new file mode 100644 index 0000000..7c3762d Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/tmp.png differ diff --git a/src/renderer/manage/pages/assets/icons/ts.png b/src/renderer/manage/pages/assets/icons/ts.png new file mode 100644 index 0000000..8e3c119 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ts.png differ diff --git a/src/renderer/manage/pages/assets/icons/ttc.png b/src/renderer/manage/pages/assets/icons/ttc.png new file mode 100644 index 0000000..723439d Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ttc.png differ diff --git a/src/renderer/manage/pages/assets/icons/ttf.png b/src/renderer/manage/pages/assets/icons/ttf.png new file mode 100644 index 0000000..0d9c8fa Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ttf.png differ diff --git a/src/renderer/manage/pages/assets/icons/txt.png b/src/renderer/manage/pages/assets/icons/txt.png new file mode 100644 index 0000000..36c466c Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/txt.png differ diff --git a/src/renderer/manage/pages/assets/icons/ufo.png b/src/renderer/manage/pages/assets/icons/ufo.png new file mode 100644 index 0000000..8f895da Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/ufo.png differ diff --git a/src/renderer/manage/pages/assets/icons/unknown.png b/src/renderer/manage/pages/assets/icons/unknown.png new file mode 100644 index 0000000..5420638 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/unknown.png differ diff --git a/src/renderer/manage/pages/assets/icons/vcd.png b/src/renderer/manage/pages/assets/icons/vcd.png new file mode 100644 index 0000000..92c6658 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/vcd.png differ diff --git a/src/renderer/manage/pages/assets/icons/vob.png b/src/renderer/manage/pages/assets/icons/vob.png new file mode 100644 index 0000000..56e58d1 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/vob.png differ diff --git a/src/renderer/manage/pages/assets/icons/voc.png b/src/renderer/manage/pages/assets/icons/voc.png new file mode 100644 index 0000000..53d8fa7 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/voc.png differ diff --git a/src/renderer/manage/pages/assets/icons/vqf.png b/src/renderer/manage/pages/assets/icons/vqf.png new file mode 100644 index 0000000..05a2b84 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/vqf.png differ diff --git a/src/renderer/manage/pages/assets/icons/vue.png b/src/renderer/manage/pages/assets/icons/vue.png new file mode 100644 index 0000000..3ff9f4b Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/vue.png differ diff --git a/src/renderer/manage/pages/assets/icons/wav.png b/src/renderer/manage/pages/assets/icons/wav.png new file mode 100644 index 0000000..523b9b6 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/wav.png differ diff --git a/src/renderer/manage/pages/assets/icons/wdl.png b/src/renderer/manage/pages/assets/icons/wdl.png new file mode 100644 index 0000000..92e34f3 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/wdl.png differ diff --git a/src/renderer/manage/pages/assets/icons/webm.png b/src/renderer/manage/pages/assets/icons/webm.png new file mode 100644 index 0000000..b62035e Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/webm.png differ diff --git a/src/renderer/manage/pages/assets/icons/webp.png b/src/renderer/manage/pages/assets/icons/webp.png new file mode 100644 index 0000000..3a6ceea Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/webp.png differ diff --git a/src/renderer/manage/pages/assets/icons/wki.png b/src/renderer/manage/pages/assets/icons/wki.png new file mode 100644 index 0000000..de05ba8 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/wki.png differ diff --git a/src/renderer/manage/pages/assets/icons/wma.png b/src/renderer/manage/pages/assets/icons/wma.png new file mode 100644 index 0000000..50079e2 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/wma.png differ diff --git a/src/renderer/manage/pages/assets/icons/wmf.png b/src/renderer/manage/pages/assets/icons/wmf.png new file mode 100644 index 0000000..05d1f82 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/wmf.png differ diff --git a/src/renderer/manage/pages/assets/icons/wmv.png b/src/renderer/manage/pages/assets/icons/wmv.png new file mode 100644 index 0000000..845793a Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/wmv.png differ diff --git a/src/renderer/manage/pages/assets/icons/wmvhd.png b/src/renderer/manage/pages/assets/icons/wmvhd.png new file mode 100644 index 0000000..c7806aa Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/wmvhd.png differ diff --git a/src/renderer/manage/pages/assets/icons/woff.png b/src/renderer/manage/pages/assets/icons/woff.png new file mode 100644 index 0000000..96ec297 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/woff.png differ diff --git a/src/renderer/manage/pages/assets/icons/woff2.png b/src/renderer/manage/pages/assets/icons/woff2.png new file mode 100644 index 0000000..9c99a16 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/woff2.png differ diff --git a/src/renderer/manage/pages/assets/icons/wps.png b/src/renderer/manage/pages/assets/icons/wps.png new file mode 100644 index 0000000..17ea80a Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/wps.png differ diff --git a/src/renderer/manage/pages/assets/icons/wpt.png b/src/renderer/manage/pages/assets/icons/wpt.png new file mode 100644 index 0000000..3ed6cac Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/wpt.png differ diff --git a/src/renderer/manage/pages/assets/icons/x_t.png b/src/renderer/manage/pages/assets/icons/x_t.png new file mode 100644 index 0000000..26eaf9f Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/x_t.png differ diff --git a/src/renderer/manage/pages/assets/icons/xls.png b/src/renderer/manage/pages/assets/icons/xls.png new file mode 100644 index 0000000..c66931c Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/xls.png differ diff --git a/src/renderer/manage/pages/assets/icons/xlsm.png b/src/renderer/manage/pages/assets/icons/xlsm.png new file mode 100644 index 0000000..cdf2af0 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/xlsm.png differ diff --git a/src/renderer/manage/pages/assets/icons/xlsx.png b/src/renderer/manage/pages/assets/icons/xlsx.png new file mode 100644 index 0000000..5dd5905 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/xlsx.png differ diff --git a/src/renderer/manage/pages/assets/icons/xlt.png b/src/renderer/manage/pages/assets/icons/xlt.png new file mode 100644 index 0000000..573b954 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/xlt.png differ diff --git a/src/renderer/manage/pages/assets/icons/xltm.png b/src/renderer/manage/pages/assets/icons/xltm.png new file mode 100644 index 0000000..af0b96a Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/xltm.png differ diff --git a/src/renderer/manage/pages/assets/icons/xltx.png b/src/renderer/manage/pages/assets/icons/xltx.png new file mode 100644 index 0000000..4950047 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/xltx.png differ diff --git a/src/renderer/manage/pages/assets/icons/xmind.png b/src/renderer/manage/pages/assets/icons/xmind.png new file mode 100644 index 0000000..5f1b3ce Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/xmind.png differ diff --git a/src/renderer/manage/pages/assets/icons/xml.png b/src/renderer/manage/pages/assets/icons/xml.png new file mode 100644 index 0000000..9fcfced Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/xml.png differ diff --git a/src/renderer/manage/pages/assets/icons/xv.png b/src/renderer/manage/pages/assets/icons/xv.png new file mode 100644 index 0000000..db8bb49 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/xv.png differ diff --git a/src/renderer/manage/pages/assets/icons/xvid.png b/src/renderer/manage/pages/assets/icons/xvid.png new file mode 100644 index 0000000..1bff154 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/xvid.png differ diff --git a/src/renderer/manage/pages/assets/icons/yaml.png b/src/renderer/manage/pages/assets/icons/yaml.png new file mode 100644 index 0000000..373f983 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/yaml.png differ diff --git a/src/renderer/manage/pages/assets/icons/yml.png b/src/renderer/manage/pages/assets/icons/yml.png new file mode 100644 index 0000000..373f983 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/yml.png differ diff --git a/src/renderer/manage/pages/assets/icons/z.png b/src/renderer/manage/pages/assets/icons/z.png new file mode 100644 index 0000000..8073e03 Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/z.png differ diff --git a/src/renderer/manage/pages/assets/icons/zip.png b/src/renderer/manage/pages/assets/icons/zip.png new file mode 100644 index 0000000..238768c Binary files /dev/null and b/src/renderer/manage/pages/assets/icons/zip.png differ diff --git a/src/renderer/manage/pages/assets/imgur.png b/src/renderer/manage/pages/assets/imgur.png new file mode 100644 index 0000000..4631b7e Binary files /dev/null and b/src/renderer/manage/pages/assets/imgur.png differ diff --git a/src/renderer/manage/pages/assets/qiniu.png b/src/renderer/manage/pages/assets/qiniu.png new file mode 100644 index 0000000..6c86264 Binary files /dev/null and b/src/renderer/manage/pages/assets/qiniu.png differ diff --git a/src/renderer/manage/pages/assets/smms.png b/src/renderer/manage/pages/assets/smms.png new file mode 100644 index 0000000..97bb61c Binary files /dev/null and b/src/renderer/manage/pages/assets/smms.png differ diff --git a/src/renderer/manage/pages/assets/tcyun.png b/src/renderer/manage/pages/assets/tcyun.png new file mode 100644 index 0000000..382ad53 Binary files /dev/null and b/src/renderer/manage/pages/assets/tcyun.png differ diff --git a/src/renderer/manage/pages/assets/upyun.png b/src/renderer/manage/pages/assets/upyun.png new file mode 100644 index 0000000..14b53da Binary files /dev/null and b/src/renderer/manage/pages/assets/upyun.png differ diff --git a/src/renderer/manage/pages/bucketPage.vue b/src/renderer/manage/pages/bucketPage.vue new file mode 100644 index 0000000..25e0e13 --- /dev/null +++ b/src/renderer/manage/pages/bucketPage.vue @@ -0,0 +1,2681 @@ +/* + *UI布局和部分样式代码参考了https://github.com/willnewii/qiniuClient + *感谢作者@willnewii + */ + + + + + diff --git a/src/renderer/manage/pages/emptyPage.vue b/src/renderer/manage/pages/emptyPage.vue new file mode 100644 index 0000000..730c709 --- /dev/null +++ b/src/renderer/manage/pages/emptyPage.vue @@ -0,0 +1,8 @@ + + + diff --git a/src/renderer/manage/pages/logIn.vue b/src/renderer/manage/pages/logIn.vue new file mode 100644 index 0000000..2ce24b8 --- /dev/null +++ b/src/renderer/manage/pages/logIn.vue @@ -0,0 +1,520 @@ + + + + + diff --git a/src/renderer/manage/pages/manageMain.vue b/src/renderer/manage/pages/manageMain.vue new file mode 100644 index 0000000..3ebef69 --- /dev/null +++ b/src/renderer/manage/pages/manageMain.vue @@ -0,0 +1,562 @@ + + + + + diff --git a/src/renderer/manage/pages/manageSetting.vue b/src/renderer/manage/pages/manageSetting.vue new file mode 100644 index 0000000..e53378a --- /dev/null +++ b/src/renderer/manage/pages/manageSetting.vue @@ -0,0 +1,558 @@ + + + + + diff --git a/src/renderer/manage/store/bucketFileDb.ts b/src/renderer/manage/store/bucketFileDb.ts new file mode 100644 index 0000000..6691911 --- /dev/null +++ b/src/renderer/manage/store/bucketFileDb.ts @@ -0,0 +1,50 @@ +import Dexie, { Table } from 'dexie' + +/* + * create a database for bucket file cache + *database name: bucketFileDb + *structure: + - table: picBedName + - key: alias-bucketName-prefix + - value: from fullList + - primaryKey: key +*/ + +export interface IFileCache { + key: string, + value: any +} + +/** + * new picbed will add a plist suffix to distinguish from picgo + */ +export class FileCacheDb extends Dexie { + tcyun: Table + aliyun: Table + qiniu: Table + github: Table + smms: Table + upyun: Table + imgur: Table + s3plist: Table + + constructor () { + super('bucketFileDb') + const tableNames = ['tcyun', 'aliyun', 'qiniu', 'github', 'smms', 'upyun', 'imgur', 's3plist'] + const tableNamesMap = tableNames.reduce((acc, cur) => { + acc[cur] = '&key, value' + return acc + }, {} as IStringKeyMap) + this.version(1).stores(tableNamesMap) + this.tcyun = this.table('tcyun') + this.aliyun = this.table('aliyun') + this.qiniu = this.table('qiniu') + this.github = this.table('github') + this.smms = this.table('smms') + this.upyun = this.table('upyun') + this.imgur = this.table('imgur') + this.s3plist = this.table('s3plist') + } +} + +export const fileCacheDbInstance = new FileCacheDb() diff --git a/src/renderer/manage/store/manageStore.ts b/src/renderer/manage/store/manageStore.ts new file mode 100644 index 0000000..86f285f --- /dev/null +++ b/src/renderer/manage/store/manageStore.ts @@ -0,0 +1,47 @@ +import { defineStore } from 'pinia' +import { getConfig } from '../utils/dataSender' + +export const useManageStore = defineStore('manageConfig', { + state: () => { + return { + config: {} as IStringKeyMap + } + }, + actions: { + async refreshConfig () { + this.config = await getConfig() ?? {} + } + }, + persist: true +}) + +export const useFileTransferStore = defineStore('fileTransfer', { + state: () => { + return { + fileTransferList: [] as IStringKeyMap[], + success: false, + finished: false + } + }, + actions: { + refreshFileTransferList (newData: IStringKeyMap) { + this.fileTransferList = newData.fullList ?? [] + this.success = newData.success + this.finished = newData.finished + }, + resetFileTransferList () { + this.fileTransferList = [] + this.success = false + this.finished = false + }, + getFileTransferList () { + return this.fileTransferList + }, + isFinished () { + return this.finished + }, + isSuccess () { + return this.success + } + } +}) diff --git a/src/renderer/manage/utils/common.ts b/src/renderer/manage/utils/common.ts new file mode 100644 index 0000000..376f1ff --- /dev/null +++ b/src/renderer/manage/utils/common.ts @@ -0,0 +1,146 @@ +import { v4 as uuidv4 } from 'uuid' +import path from 'path' +import crypto from 'crypto' +import { availableIconList } from './icon' + +export function randomStringGenerator (length: number): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + return Array.from({ length }).map(() => chars.charAt(Math.floor(Math.random() * chars.length))).join('') +} + +export function renameFileNameWithTimestamp (oldName: string): string { + return `${Math.floor(Date.now() / 1000)}${randomStringGenerator(5)}${path.extname(oldName)}` +} + +export function renameFileNameWithRandomString (oldName: string, length: number = 5): string { + return `${randomStringGenerator(length)}${path.extname(oldName)}` +} + +export function renameFileNameWithCustomString (oldName: string, customFormat: string): string { + const conversionMap : {[key: string]: () => string} = { + '{Y}': () => new Date().getFullYear().toString(), + '{y}': () => new Date().getFullYear().toString().slice(2), + '{m}': () => (new Date().getMonth() + 1).toString().length === 1 ? `0${new Date().getMonth() + 1}` : (new Date().getMonth() + 1).toString(), + '{d}': () => new Date().getDate().toString().length === 1 ? `0${new Date().getDate()}` : new Date().getDate().toString(), + '{md5}': () => crypto.createHash('md5').update(path.basename(oldName, path.extname(oldName))).digest('hex'), + '{md5-16}': () => crypto.createHash('md5').update(path.basename(oldName, path.extname(oldName))).digest('hex').slice(0, 16), + '{str-10}': () => randomStringGenerator(10), + '{str-20}': () => randomStringGenerator(20), + '{filename}': () => path.basename(oldName, path.extname(oldName)), + '{uuid}': () => uuidv4().replace(/-/g, ''), + '{timestamp}': () => Math.floor(Date.now() / 1000).toString() + } + if (customFormat === undefined || !Object.keys(conversionMap).some(item => customFormat.includes(item))) { + return oldName + } + const ext = path.extname(oldName) + return Object.keys(conversionMap).reduce((acc, cur) => { + return acc.replace(cur, conversionMap[cur]()) + }, customFormat) + ext +} + +export function renameFile (typeMap : IStringKeyMap, oldName: string): string { + if (typeMap.timestampRename) { + return renameFileNameWithTimestamp(oldName) + } else if (typeMap.randomStringRename) { + return renameFileNameWithRandomString(oldName, 20) + } else { + return renameFileNameWithCustomString(oldName, typeMap.customRenameFormat) + } +} + +export function formatLink (url: string, fileName: string, type: string, format?: string) : string { + switch (type) { + case 'markdown': + return `![${fileName}](${url})` + case 'html': + return `${fileName}` + case 'bbcode': + return `[img]${url}[/img]` + case 'url': + return url + case 'markdown-with-link': + return `[![${fileName}](${url})](${url})` + case 'custom': + if (format && (format.includes('$url') || format.includes('$fileName'))) { + return format.replace(/\$url/g, url).replace(/\$fileName/g, fileName) + } + return url + default: + return url + } +} + +export function getFileIconPath (fileName: string) { + const ext = path.extname(fileName).slice(1) + return availableIconList.includes(ext) ? `${ext}.png` : 'unknown.png' +} + +export function formatFileSize (size: number) { + if (size === 0) return '' + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + const index = Math.floor(Math.log2(size) / 10) + return `${(size / Math.pow(2, index * 10)).toFixed(2)} ${units[index]}` +} + +export function formatFileName (fileName: string) { + const ext = path.extname(fileName) + const name = path.basename(fileName, ext) + return name.length > 20 ? `${name.slice(0, 20)}...${ext}` : fileName +} + +export function getExtension (fileName: string) { + return path.extname(fileName).slice(1) +} + +export function isImage (fileName: string) { + return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico'].includes(getExtension(fileName)) +} + +export function formObjToTableData (obj: any) { + const exclude = [undefined, null, '', 'transformedConfig'] + return Object.keys(obj).filter(key => !exclude.includes(obj[key])).map(key => ({ + key, + value: typeof obj[key] === 'object' ? JSON.stringify(obj[key]) : obj[key] + })).sort((a, b) => a.key.localeCompare(b.key)) +} + +export function isValidUrl (str: string) { + const pattern = new RegExp( + '^([a-zA-Z]+:\\/\\/)?' + + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + + '((\\d{1,3}\\.){3}\\d{1,3}))' + + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + + '(\\?[;&a-z\\d%_.~+=-]*)?' + + '(\\#[-a-z\\d_]*)?$', + 'i' + ) + return pattern.test(str) +} + +export interface IHTTPProxy { + host: string + port: number + protocol: string +} + +export const formatHttpProxy = (proxy: string | undefined, type: 'object' | 'string'): IHTTPProxy | undefined | string => { + if (proxy === undefined || proxy === '') return undefined + if (proxy.startsWith('http://') || proxy.startsWith('https://')) { + const { protocol, hostname, port } = new URL(proxy) + if (type === 'string') return `${protocol}//${hostname}:${port}` + return { + host: hostname, + port: Number(port), + protocol: protocol.slice(0, -1) + } + } else { + const [host, port] = proxy.split(':') + if (type === 'string') return `http://${host}:${port}` + return { + host, + port: port ? Number(port) : 80, + protocol: 'http' + } + } +} diff --git a/src/renderer/manage/utils/constants.ts b/src/renderer/manage/utils/constants.ts new file mode 100644 index 0000000..a487f51 --- /dev/null +++ b/src/renderer/manage/utils/constants.ts @@ -0,0 +1,501 @@ + +const defaultBaseRule = (name: string) => { + return [ + { + required: true, + message: `请输入${name}`, + trigger: 'blur' + } + ] +} + +const itemsPerPageRule = [ + { + required: true, + message: '请输入每页显示数量', + trigger: 'blur' + }, + { + type: 'number', + message: '每页显示数量必须为数字', + trigger: 'change' + }, + { + validator: (rule: any, value: any, callback: any) => { + if (value < 20 || value > 1000) { + callback(new Error('每页显示数量必须在20-1000之间')) + } else { + callback() + } + }, + trigger: 'change' + } +] + +const aliasRule = [ + { + required: true, + message: '请输入配置别名, 该配置的唯一标识', + trigger: 'blur' + }, + { + validator: (rule: any, value: any, callback: any) => { + const reg = /^[\u4e00-\u9fa5_a-zA-Z0-9-]{1,15}$/ + if (!reg.test(value)) { + callback(new Error('配置别名只能包含中文、英文、数字和下划线,且不能超过15个字符')) + } else { + callback() + } + }, + trigger: 'change' + } +] + +export const supportedPicBedList: IStringKeyMap = { + smms: { + name: 'SM.MS', + icon: 'smms', + configOptions: { + alias: { + required: true, + description: '配置别名-必需', + placeholder: '该配置的唯一标识', + type: 'string', + rule: aliasRule, + default: 'smms-A' + }, + token: { + required: true, + description: 'token-必需', + placeholder: '请输入token', + type: 'string', + rule: defaultBaseRule('token') + }, + paging: { + required: true, + description: '是否分页', + default: true, + type: 'boolean' + } + }, + explain: '大陆地区请访问备用域名https://smms.app, 请勿大批量上传图片,否则API接口会被限制', + options: ['alias', 'token', 'paging'], + refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=%e5%8f%82%e6%95%b0%e8%af%b4%e6%98%8e-6', + referenceText: '配置教程请参考:' + }, + qiniu: { + name: '七牛云', + icon: 'qiniu', + configOptions: { + alias: { + required: true, + description: '配置别名-必需', + placeholder: '该配置的唯一标识', + type: 'string', + rule: aliasRule, + default: 'qiniu-A' + }, + accessKey: { + required: true, + description: 'accessKey-必需', + placeholder: '请输入accessKey', + type: 'string', + rule: defaultBaseRule('accessKey') + }, + secretKey: { + required: true, + description: 'secretKey-必需', + placeholder: '请输入secretKey', + type: 'string', + rule: defaultBaseRule('secretKey') + }, + bucketName: { + required: false, + description: '空间名-可选', + placeholder: '英文逗号分隔,例如:bucket1,bucket2', + type: 'string' + }, + baseDir: { + required: false, + description: '起始目录-可选', + placeholder: '英文逗号分隔,例如:/test1,/test2', + default: '/', + type: 'string' + }, + paging: { + required: true, + description: '是否分页', + default: true, + type: 'boolean' + }, + itemsPerPage: { + required: true, + description: '每页显示数量', + default: 50, + type: 'number', + rule: itemsPerPageRule + } + }, + explain: '空间名和起始目录配置时可通过英文逗号分隔不同存储桶的设置,顺序必须一致,逗号间留空或缺失项使用默认值', + options: ['alias', 'accessKey', 'secretKey', 'bucketName', 'baseDir', 'paging', 'itemsPerPage'], + refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=%e5%8f%82%e6%95%b0%e8%af%b4%e6%98%8e-3', + referenceText: '配置教程请参考:' + }, + github: { + name: 'GitHub', + icon: 'github', + configOptions: { + alias: { + required: true, + description: '配置别名-必需', + placeholder: '该配置的唯一标识', + type: 'string', + rule: aliasRule, + default: 'github-A' + }, + token: { + required: true, + description: 'token-必需', + placeholder: '请输入token', + type: 'string', + rule: defaultBaseRule('token') + }, + githubUsername: { + required: true, + description: '用户名-必需', + placeholder: '请输入用户名', + type: 'string', + rule: defaultBaseRule('用户名') + }, + proxy: { + required: false, + description: '代理-可选', + placeholder: '例如:http://127.0.0.1:1080', + type: 'string' + }, + paging: { + required: true, + description: '是否分页', + default: false, + type: 'boolean' + }, + customUrl: { + required: false, + description: 'CDN加速域名-可选;例如: https://cdn.staticaly.com/gh/{username}/{repo}@{branch}/{path}', + placeholder: '支持使用{username}、{repo}、{branch}和{path}作为替换占位符,用于适配不同仓库和分支', + type: 'string', + rule: [ + { + validator: (_rule: any, value: any, callback: any) => { + if (value) { + const customUrlList = value.split(',') + const customUrlValid = customUrlList.every((customUrl: string) => { + const reg = /^((https|http)?:\/\/)/ + if (customUrl === '') { + return true + } else if (!reg.test(customUrl)) { + return false + } + return true + }) + const isBracketsValid = customUrlList.every((customUrl: string) => { + const bracketPaired = (str: string) => { + const stack = [] + for (let i = 0; i < str.length; i++) { + if (str[i] === '{') { + stack.push(str[i]) + } else if (str[i] === '}') { + if (stack.length === 0) { + return false + } + stack.pop() + } + } + return stack.length === 0 + } + if (customUrl === '') { + return true + } else if (!bracketPaired(customUrl)) { + return false + } + return true + }) + if (!customUrlValid) { + callback(new Error('加速域名请以http://或https://开头')) + } else if (!isBracketsValid) { + callback(new Error('加速域名中的大括号必须成对出现')) + } else { + callback() + } + } else { + callback() + } + }, + trigger: 'change' + } + ] + } + }, + explain: 'API调用有每小时上限,此外不支持上传超过100M的文件', + options: ['alias', 'token', 'githubUsername', 'proxy', 'customUrl'], + refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=%e5%8f%82%e6%95%b0%e8%af%b4%e6%98%8e-9', + referenceText: '配置教程请参考:' + }, + aliyun: { + name: '阿里云', + icon: 'aliyun', + configOptions: { + alias: { + required: true, + description: '配置别名-必需', + placeholder: '该配置的唯一标识', + type: 'string', + rule: aliasRule, + default: 'aliyun-A' + }, + accessKeyId: { + required: true, + description: 'accessKeyId-必需', + placeholder: '请输入accessKeyId', + type: 'string', + rule: defaultBaseRule('accessKeyId') + }, + accessKeySecret: { + required: true, + description: 'accessKeySecret-必需', + placeholder: '请输入accessKeySecret', + type: 'string', + rule: defaultBaseRule('accessKeySecret') + }, + bucketName: { + required: false, + description: '存储桶名-可选', + placeholder: '英文逗号分隔,例如:bucket1,bucket2', + type: 'string' + }, + baseDir: { + required: false, + description: '起始目录-可选', + placeholder: '英文逗号分隔,例如:/test1,/test2', + type: 'string', + default: '/' + }, + paging: { + required: true, + description: '是否分页', + default: true, + type: 'boolean' + }, + itemsPerPage: { + required: true, + description: '每页显示数量', + default: 50, + type: 'number', + rule: itemsPerPageRule + } + }, + explain: '存储桶名和起始目录配置时可通过英文逗号分隔不同存储桶的设置,顺序必须一致,逗号间留空或缺失项使用默认值', + options: ['alias', 'accessKeyId', 'accessKeySecret', 'bucketName', 'baseDir', 'paging', 'itemsPerPage'], + refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=%e5%8f%82%e6%95%b0%e8%af%b4%e6%98%8e-1', + referenceText: '配置教程请参考:' + }, + tcyun: { + name: '腾讯云', + icon: 'tcyun', + configOptions: { + alias: { + required: true, + description: '配置别名-必需', + placeholder: '该配置的唯一标识', + type: 'string', + rule: aliasRule, + default: 'tcyun-A' + }, + secretId: { + required: true, + description: 'secretId-必需', + placeholder: '请输入secretId', + type: 'string', + rule: defaultBaseRule('secretId') + }, + secretKey: { + required: true, + description: 'secretKey-必需', + placeholder: '请输入secretKey', + type: 'string', + rule: defaultBaseRule('secretKey') + }, + appId: { + required: true, + description: 'appId-必需', + placeholder: '请输入appId', + type: 'string', + rule: defaultBaseRule('appId') + }, + bucketName: { + required: false, + description: '存储桶名-可选(注意包含AppId)', + placeholder: '英文逗号分隔,例如:bucket1-1250000000,bucket2-1250000000', + type: 'string' + }, + baseDir: { + required: false, + description: '起始目录-可选', + placeholder: '英文逗号分隔,例如:/test1,/test2', + type: 'string', + default: '/' + }, + paging: { + required: true, + description: '是否分页', + default: true, + type: 'boolean' + }, + itemsPerPage: { + required: true, + description: '每页显示数量', + default: 50, + type: 'number', + rule: itemsPerPageRule + } + }, + explain: '存储桶名和起始目录配置时可通过英文逗号分隔不同存储桶的设置,顺序必须一致,逗号间留空或缺失项使用默认值', + options: ['alias', 'secretId', 'secretKey', 'appId', 'bucketName', 'baseDir', 'paging', 'itemsPerPage'], + refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=%e5%8f%82%e6%95%b0%e8%af%b4%e6%98%8e-2', + referenceText: '配置教程请参考:' + }, + upyun: { + name: '又拍云', + icon: 'upyun', + configOptions: { + alias: { + required: true, + description: '配置别名-必需', + placeholder: '该配置的唯一标识', + type: 'string', + rule: aliasRule, + default: 'upyun-A' + }, + bucketName: { + required: true, + description: '服务名-必需', + placeholder: '对应其它对象存储的存储桶名', + type: 'string', + rule: defaultBaseRule('bucketName') + }, + operator: { + required: true, + description: '操作员-必需', + placeholder: '推荐使用具有读取、写入和删除完整权限的操作员', + type: 'string', + rule: defaultBaseRule('操作员') + }, + password: { + required: true, + description: '操作员密码-必需', + placeholder: '请输入密码', + type: 'string', + rule: defaultBaseRule('操作员密码') + }, + baseDir: { + required: false, + description: '起始目录-可选', + placeholder: '读取文件时的初始目录', + type: 'string', + default: '/' + }, + customUrl: { + required: true, + description: '加速域名-必需', + placeholder: '请以http://或https://开头', + type: 'string', + rule: [ + { + required: true, + message: '加速域名不能为空', + trigger: 'change' + }, + { + validator: (rule: any, value: any, callback: any) => { + if (value) { + const customUrlList = value.split(',') + const customUrlValid = customUrlList.every((customUrl: string) => { + const reg = /^((https|http)?:\/\/)/ + if (customUrl === '') { + return true + } else if (!reg.test(customUrl)) { + return false + } + return true + }) + if (!customUrlValid) { + callback(new Error('自定义域名请以http://或https://开头')) + } else { + callback() + } + } else { + callback() + } + }, + trigger: 'change' + } + ] + }, + paging: { + required: true, + description: '是否分页', + default: true, + type: 'boolean' + }, + itemsPerPage: { + required: true, + description: '每页显示数量', + default: 50, + type: 'number', + rule: itemsPerPageRule + } + }, + explain: '又拍云图床务必填写加速域名,否则无法正常使用', + options: ['alias', 'bucketName', 'operator', 'password', 'baseDir', 'customUrl', 'paging', 'itemsPerPage'], + refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=%e5%8f%82%e6%95%b0%e8%af%b4%e6%98%8e-4', + referenceText: '配置教程请参考:' + }, + imgur: { + name: 'Imgur', + icon: 'imgur', + configOptions: { + alias: { + required: true, + description: '配置别名-必需', + placeholder: '该配置的唯一标识', + type: 'string', + rule: aliasRule, + default: 'imgur-A' + }, + imgurUserName: { + required: true, + description: 'imgur用户名-必需', + placeholder: '请输入imgur用户名', + type: 'string', + rule: defaultBaseRule('imgurUserName') + }, + accessToken: { + required: true, + description: 'accessToken-必需(不是clientID,请参考配置教程)', + placeholder: '请输入accessToken', + type: 'string', + rule: defaultBaseRule('accessToken') + }, + proxy: { + required: false, + description: '代理-可选', + placeholder: '例如:http://127.0.0.1:1080', + type: 'string' + } + }, + explain: '大陆地区请使用代理,API调用存在限制,请注意使用频率', + options: ['alias', 'imgurUserName', 'accessToken', 'proxy'], + refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=imgur%e5%9b%be%e5%ba%8a-1', + referenceText: '配置教程请参考:' + } +} diff --git a/src/renderer/manage/utils/dataSender.ts b/src/renderer/manage/utils/dataSender.ts new file mode 100644 index 0000000..887138f --- /dev/null +++ b/src/renderer/manage/utils/dataSender.ts @@ -0,0 +1,44 @@ +import { ipcRenderer, IpcRendererEvent } from 'electron' +import { PICLIST_MANAGE_GET_CONFIG, PICLIST_MANAGE_SAVE_CONFIG, PICLIST_MANAGE_REMOVE_CONFIG } from '~/main/manage/events/constants' +import { v4 as uuid } from 'uuid' +import { getRawData } from '~/renderer/utils/common' + +export function getConfig (key?: string): Promise { + return new Promise((resolve) => { + const callbackId = uuid() + const callback = (event: IpcRendererEvent, config: T | undefined, returnCallbackId: string) => { + if (returnCallbackId === callbackId) { + resolve(config) + ipcRenderer.removeListener(PICLIST_MANAGE_GET_CONFIG, callback) + } + } + ipcRenderer.on(PICLIST_MANAGE_GET_CONFIG, callback) + ipcRenderer.send(PICLIST_MANAGE_GET_CONFIG, key, callbackId) + }) +} + +export function saveConfig (_config: IObj | string, value?: any) { + let config + if (typeof _config === 'string') { + config = { + [_config]: value + } + } else { + config = getRawData(_config) + } + ipcRenderer.send(PICLIST_MANAGE_SAVE_CONFIG, config) +} + +export function removeConfig (key: string, propName: string) { + ipcRenderer.send(PICLIST_MANAGE_REMOVE_CONFIG, key, propName) +} + +export function sendToMain (channel: string, ...args: any[]) { + const data = getRawData(args) + ipcRenderer.send(channel, ...data) +} + +export function invokeToMain (channel: string, ...args: any[]) { + const data = getRawData(args) + return ipcRenderer.invoke(channel, ...data) +} diff --git a/src/renderer/manage/utils/icon.ts b/src/renderer/manage/utils/icon.ts new file mode 100644 index 0000000..cd47e91 --- /dev/null +++ b/src/renderer/manage/utils/icon.ts @@ -0,0 +1,224 @@ +export const availableIconList = [ + '_blank', + '_page', + '3g2', + '3gp', + '7z', + 'aac', + 'accdb', + 'adt', + 'ai', + 'aiff', + 'aly', + 'amiga', + 'amr', + 'ape', + 'apk', + 'arj', + 'asf', + 'asm', + 'asx', + 'au', + 'avc', + 'avi', + 'avs', + 'bak', + 'bas', + 'bat', + 'bmp', + 'bom', + 'c', + 'cda', + 'cdr', + 'chm', + 'class', + 'cmd', + 'com', + 'cpp', + 'css', + 'csv', + 'dart', + 'dat', + 'ddb', + 'dif', + 'divx', + 'dll', + 'dmg', + 'doc', + 'docm', + 'docx', + 'dot', + 'dotm', + 'dotx', + 'dsl', + 'dv', + 'dvd', + 'dvdaudio', + 'dwg', + 'dxf', + 'emf', + 'env', + 'eot', + 'eps', + 'exe', + 'exif', + 'fakesmms', + 'flc', + 'fli', + 'flv', + 'folder', + 'fon', + 'font', + 'for', + 'fpx', + 'fv', + 'gif', + 'gitingore', + 'gitkeep', + 'gz', + 'h', + 'hdri', + 'hlp', + 'hpp', + 'htm', + 'html', + 'ico', + 'ics', + 'int', + 'ipynb', + 'iso', + 'java', + 'jpeg', + 'jpg', + 'js', + 'json', + 'key', + 'ksp', + 'less', + 'lib', + 'lic', + 'license', + 'log', + 'lst', + 'lua', + 'mac', + 'map', + 'markdown', + 'md', + 'mdf', + 'mht', + 'mhtml', + 'mid', + 'midi', + 'mkv', + 'mmf', + 'mod', + 'mov', + 'mp2', + 'mp3', + 'mp4', + 'mpa', + 'mpe', + 'mpeg', + 'mpeg1', + 'mpeg2', + 'mpg', + 'mppro', + 'msg', + 'mts', + 'mux', + 'mv', + 'navi', + 'obj', + 'odf', + 'ods', + 'odt', + 'ogg', + 'one', + 'otf', + 'otp', + 'ots', + 'ott', + 'pas', + 'pcd', + 'pcx', + 'pdf', + 'php', + 'pic', + 'png', + 'ppt', + 'pptx', + 'proe', + 'prt', + 'psd', + 'py', + 'pyc', + 'qsv', + 'qt', + 'quicktime', + 'ra', + 'ram', + 'rar', + 'raw', + 'rb', + 'realaudio', + 'rm', + 'rmvb', + 'rp', + 'rtf', + 's48', + 'sacd', + 'sass', + 'sch', + 'scss', + 'sh', + 'sql', + 'stp', + 'svcd', + 'svg', + 'swf', + 'sys', + 'tga', + 'tgz', + 'tiff', + 'tmp', + 'ts', + 'ttc', + 'ttf', + 'txt', + 'ufo', + 'unknown', + 'vcd', + 'vob', + 'voc', + 'vqf', + 'vue', + 'wav', + 'wdl', + 'webm', + 'webp', + 'wki', + 'wma', + 'wmf', + 'wmv', + 'wmvhd', + 'woff', + 'woff2', + 'wps', + 'wpt', + 'x_t', + 'xls', + 'xlsm', + 'xlsx', + 'xlt', + 'xltm', + 'xltx', + 'xmind', + 'xml', + 'xv', + 'xvid', + 'yaml', + 'yml', + 'z', + 'zip' +] diff --git a/src/renderer/manage/utils/newBucketConfig.ts b/src/renderer/manage/utils/newBucketConfig.ts new file mode 100644 index 0000000..3ab6320 --- /dev/null +++ b/src/renderer/manage/utils/newBucketConfig.ts @@ -0,0 +1,227 @@ +import { AliyunAreaCodeName, QiniuAreaCodeName, TencentAreaCodeName } from '~/main/manage/utils/constants' + +export const newBucketConfig:IStringKeyMap = { + tcyun: { + name: '腾讯云', + icon: 'tcyun', + configOptions: { + BucketName: { + required: true, + description: 'Bucket名称', + placeholder: '请输入Bucket名称', + paraType: 'string', + component: 'input', + default: 'piclist', + rule: [ + { + required: true, + message: 'Bucket名称不能为空', + trigger: 'blur' + }, + { + validator: (rule: any, value: any, callback: any) => { + const reg = /^[a-z0-9][a-z0-9-]{1,21}[a-z0-9]$/ + if (value.length > 23) { + callback(new Error('Bucket名称长度不能超过23个字符')) + } else if (!reg.test(value)) { + callback(new Error('Bucket名称只能包含小写字母、数字和中划线,且不能以中划线开头和结尾')) + } else { + callback() + } + }, + trigger: 'change' + } + ] + }, + region: { + required: true, + description: '地域', + paraType: 'string', + component: 'select', + default: 'ap-nanjing', + options: TencentAreaCodeName + }, + acl: { + required: true, + description: '访问权限', + paraType: 'string', + component: 'select', + default: 'private', + options: { + private: '私有', + publicRead: '公共读', + publicReadWrite: '公共读写' + } + } + }, + options: ['BucketName', 'region', 'acl'] + }, + aliyun: { + name: '阿里云', + icon: 'aliyun', + configOptions: { + BucketName: { + required: true, + description: 'Bucket名称', + placeholder: '请输入Bucket名称', + paraType: 'string', + component: 'input', + default: 'piclist', + rule: [ + { + required: true, + message: 'Bucket名称不能为空', + trigger: 'blur' + }, + { + validator: (rule: any, value: any, callback: any) => { + const reg = /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/ + if (value.length > 63) { + callback(new Error('Bucket名称长度不能超过63个字符')) + } else if (!reg.test(value)) { + callback(new Error('Bucket名称只能包含小写字母、数字和中划线,且不能以中划线开头和结尾')) + } else { + callback() + } + }, + trigger: 'change' + } + ] + }, + region: { + required: true, + description: '地域', + paraType: 'string', + component: 'select', + default: 'oss-cn-hangzhou', + options: AliyunAreaCodeName + }, + acl: { + required: true, + description: '访问权限', + paraType: 'string', + component: 'select', + default: 'private', + options: { + private: '私有', + publicRead: '公共读', + publicReadWrite: '公共读写' + } + } + }, + options: ['BucketName', 'region', 'acl'] + }, + qiniu: { + name: '七牛云', + icon: 'qiniu', + configOptions: { + BucketName: { + required: true, + description: 'Bucket名称', + placeholder: '请输入Bucket名称', + paraType: 'string', + component: 'input', + default: 'piclist', + rule: [ + { + required: true, + message: 'Bucket名称不能为空', + trigger: 'blur' + }, + { + validator: (rule: any, value: any, callback: any) => { + const reg = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/ + if (value.length > 63) { + callback(new Error('Bucket名称长度不能超过63个字符')) + } else if (!reg.test(value)) { + callback(new Error('Bucket名称只能包含小写字母、数字和中划线,且不能以中划线开头和结尾')) + } else { + callback() + } + }, + trigger: 'change' + } + ] + }, + region: { + required: true, + description: '地域', + paraType: 'string', + component: 'select', + default: 'z0', + options: QiniuAreaCodeName + }, + acl: { + required: true, + description: '公开访问', + paraType: 'boolean', + component: 'switch', + default: false + } + }, + options: ['BucketName', 'region', 'acl'] + }, + upyun: { + name: '又拍云', + icon: 'upyun', + configOptions: { + BucketName: { + required: true, + description: 'Bucket名称', + placeholder: '请输入Bucket名称', + paraType: 'string', + component: 'input', + default: 'piclist', + rule: [ + { + required: true, + message: 'Bucket名称不能为空', + trigger: 'blur' + }, + { + validator: (rule: any, value: any, callback: any) => { + const reg = /^[a-z][a-z0-9-]{4,19}$/ + if (value.length > 23 || value.length < 5) { + callback(new Error('Bucket名称长度为5-20个字符')) + } else if (!reg.test(value)) { + callback(new Error('Bucket名称只能包含小写字母、数字和中划线,且不能以中划线开头和结尾')) + } else { + callback() + } + }, + trigger: 'change' + } + ] + }, + operator: { + required: true, + description: '操作员', + placeholder: '请输入操作员', + paraType: 'string', + component: 'input', + rule: [ + { + required: true, + message: '操作员不能为空', + trigger: 'blur' + } + ] + }, + password: { + required: true, + description: '密码', + placeholder: '请输入密码', + paraType: 'string', + component: 'input', + rule: [ + { + required: true, + message: '密码不能为空', + trigger: 'blur' + } + ] + } + }, + options: ['BucketName', 'operator', 'password'] + } +} diff --git a/src/renderer/pages/Gallery.vue b/src/renderer/pages/Gallery.vue index 10323e7..491764c 100644 --- a/src/renderer/pages/Gallery.vue +++ b/src/renderer/pages/Gallery.vue @@ -10,6 +10,15 @@ + 同步删除云端: + @@ -79,7 +88,7 @@
{{ $T('COPY') }} @@ -88,7 +97,7 @@
{{ $T('DELETE') }} @@ -97,10 +106,10 @@
- {{ isAllSelected ? $T('CANCEL') : $T('SELECT_ALL') }} + {{ isAllSelected? $T('CANCEL'): $T('SELECT_ALL') }}
@@ -128,7 +137,11 @@
@@ -187,9 +200,7 @@ :modal-append-to-body="false" > -