mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-08-31 13:07:03 +08:00
Merge branch 'dev' into release
This commit is contained in:
+9
-5
@@ -1,5 +1,9 @@
|
||||
test/unit/coverage/**
|
||||
test/unit/*.js
|
||||
test/e2e/*.js
|
||||
dist/
|
||||
src/**/*.js
|
||||
test/unit/coverage/**
|
||||
test/unit/*.js
|
||||
test/e2e/*.js
|
||||
dist/
|
||||
src/**/*.js
|
||||
node_modules
|
||||
dist
|
||||
vue.config.js
|
||||
babel.config.js
|
||||
|
||||
+20
-3
@@ -10,7 +10,8 @@ module.exports = {
|
||||
extends: [
|
||||
'plugin:vue/vue3-recommended',
|
||||
'@vue/standard',
|
||||
'@vue/typescript'
|
||||
'@vue/typescript/recommended',
|
||||
'@vue/eslint-config-prettier'
|
||||
],
|
||||
plugins: ['@typescript-eslint'],
|
||||
rules: {
|
||||
@@ -20,8 +21,24 @@ module.exports = {
|
||||
'no-async-promise-executor': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'@typescript-eslint/indent': ['error', 2],
|
||||
'vue/no-v-html': 'off'
|
||||
'@typescript-eslint/indent': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'error',
|
||||
{
|
||||
'ts-expect-error': 'allow-with-description',
|
||||
'ts-nocheck': 'allow-with-description'
|
||||
}
|
||||
],
|
||||
'vue/no-v-html': 'off',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{},
|
||||
{
|
||||
usePrettierrc: true
|
||||
}
|
||||
]
|
||||
},
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120,
|
||||
"proseWrap": "always",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
Vendored
+34
-35
@@ -1,36 +1,35 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Electron: Main",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
|
||||
},
|
||||
"preLaunchTask": "electron-debug",
|
||||
"args": ["--remote-debugging-port=9223", "./dist_electron"],
|
||||
"outFiles": ["${workspaceFolder}/dist_electron/**/*.js"]
|
||||
},
|
||||
{
|
||||
"name": "Electron: Renderer",
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"port": 9223,
|
||||
"urlFilter": "http://localhost:*",
|
||||
"timeout": 30000,
|
||||
"webRoot": "${workspaceFolder}/src",
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///./src/*": "${webRoot}/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Electron: All",
|
||||
"configurations": ["Electron: Main", "Electron: Renderer"]
|
||||
}
|
||||
]
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Electron: Main",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
|
||||
},
|
||||
"preLaunchTask": "electron-debug",
|
||||
"args": ["--remote-debugging-port=9223", "./dist_electron"],
|
||||
"outFiles": ["${workspaceFolder}/dist_electron/**/*.js"]
|
||||
},
|
||||
{
|
||||
"name": "Electron: Renderer",
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"port": 9223,
|
||||
"urlFilter": "http://localhost:*",
|
||||
"timeout": 30000,
|
||||
"webRoot": "${workspaceFolder}/src",
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///./src/*": "${webRoot}/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Electron: All",
|
||||
"configurations": ["Electron: Main", "Electron: Renderer"]
|
||||
}
|
||||
]
|
||||
}
|
||||
+1993
-1967
File diff suppressed because it is too large
Load Diff
+33
-33
@@ -1,33 +1,33 @@
|
||||
# 贡献指南
|
||||
|
||||
## 安装与启动
|
||||
|
||||
1. 使用 [yarn](https://yarnpkg.com/) 安装依赖
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
然后通过
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
启动项目。
|
||||
|
||||
2. 只跟 Electron 主进程相关的代码请在 `src/main` 目录下添加。只跟渲染进程相关的代码请在 `src/renderer` 目录下添加。两个进程都能使用的代码请在 `src/universal` 目录下添加。 **注意**:渲染进程没有 `Node.js` 能力,所有渲染进程需要用到 `Node.js` 模块相关的代码请在 `src/main/events/picgoCoreIPC.ts` 下添加事件进行处理。
|
||||
3. 所有的跨进程事件名请在 `src/universal/events/constants.ts` 里添加。
|
||||
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` 编写语言文件。并注意,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`,唤起 [代码提交规范工具](https://github.com/PicGo/bump-version)。通过该工具提交代码。
|
||||
# 贡献指南
|
||||
|
||||
## 安装与启动
|
||||
|
||||
1. 使用 [yarn](https://yarnpkg.com/) 安装依赖
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
然后通过
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
启动项目。
|
||||
|
||||
1. 只跟 Electron 主进程相关的代码请在 `src/main` 目录下添加。只跟渲染进程相关的代码请在 `src/renderer` 目录下添加。两个进程都能使用的代码请在 `src/universal` 目录下添加。
|
||||
2. 所有的跨进程事件名请在 `src/universal/events/constants.ts` 里添加。
|
||||
3. 所有的全局类型定义请在 `src/universal/types/` 里添加,如果是 `enum`,请在 `src/universal/types/enum.ts` 里添加。
|
||||
4. 与图床管理功能相关的代码请在 `src/main/manage`和 `src/renderer/manage`目录下添加。
|
||||
|
||||
## 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 i18n`,确保能生成正确的语言定义文件。
|
||||
|
||||
## 提交代码
|
||||
|
||||
1. 请检查代码没有多余的注释、`console.log` 等调试代码。
|
||||
2. 提交代码前,请执行命令 `git add . && yarn cz`,唤起 [代码提交规范工具](https://github.com/Kuingsmile/node-bump-version)。通过该工具提交代码。
|
||||
|
||||
+33
-33
@@ -1,33 +1,33 @@
|
||||
# Contribution Guidelines
|
||||
|
||||
## Installation and startup
|
||||
|
||||
1. Use [yarn](https://yarnpkg.com/) to install dependencies
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
then pass
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Startup project.
|
||||
|
||||
2. Please add code only related to the main process of Electron in the `src/main` directory. Code only related to the rendering process should be added in the `src/renderer` directory. Add code that can be used by both processes in the `src/universal` directory. **Note**: The rendering process does not have the `Node.js` capability. All rendering processes need to use `Node.js modules` related code, please add events under `src/main/events/picgoCoreIPC.ts` for processing.
|
||||
3. Please add all cross-process event names in `src/universal/events/constants.ts`.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 [Code Submission Specification Tool](https://github.com/PicGo/bump-version). Submit code through this tool.
|
||||
# Contribution Guidelines
|
||||
|
||||
## Installation and startup
|
||||
|
||||
1. Use [yarn](https://yarnpkg.com/) to install dependencies
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
then pass
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Startup project.
|
||||
|
||||
1. Please add code only related to the main process of Electron in the `src/main` directory. Code only related to the rendering process should be added in the `src/renderer` directory. Add code that can be used by both processes in the `src/universal` directory.
|
||||
2. Please add all cross-process event names in `src/universal/events/constants.ts`.
|
||||
3. Please add all global type definitions in `src/universal/types/`, if it is `enum`, please add it in `src/universal/types/enum.ts`.
|
||||
4. Code related to the management function of the picture bed should be added in the `src/main/manage` and `src/renderer/manage` directory.
|
||||
|
||||
## 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 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 i18n` after the update to ensure that the correct language definition file can be generated.
|
||||
|
||||
## 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 [Code Submission Specification Tool](https://github.com/Kuingsmile/node-bump-version). Submit code through this tool.
|
||||
|
||||
@@ -1,174 +1,167 @@
|
||||
# FAQ
|
||||
|
||||
This FAQ has been modified from PicGo's FAQ, and thanks to the author Molunerfinn for PicGo.
|
||||
|
||||
## Common Questions
|
||||
|
||||
> Please refer to [user manual](https://piclist.cn/en) for general configuration issues.
|
||||
|
||||
## 1. What is the relationship between PicList and PicGo?
|
||||
|
||||
PicList is forked from the PicGo project and is based on PicGo for secondary development. At the same time, the core function kernel PicGo-Core has been developed for the second time and renamed as [PicList-Core](https://github.com/Kuingsmile/PicList-Core).
|
||||
|
||||
The addition of all new features to PicList has not affected PicGo's existing functions, so you can use all PicGo plugins in PicList. At the same time, it can still be used in conjunction with software such as Typora and Obsidian.
|
||||
|
||||
## 2. When using the image hosting management function, errors such as "unable to retrieve directory" occur
|
||||
|
||||
Please check the log file `manage.log`. In addition, the API calls of various platforms have a limit on the number of calls per hour. If an error occurs, please try again later.
|
||||
|
||||
## 3. Which remote image hosting deletion is supported?
|
||||
|
||||
Currently, the supported image hosting platforms are:
|
||||
|
||||
- Aliyun OSS
|
||||
- Tencent Cloud COS
|
||||
- Qiniu Cloud Kodo
|
||||
- Upyun
|
||||
- SM.MS
|
||||
- Imgur
|
||||
- GitHub
|
||||
- Webdav
|
||||
- Aws S3
|
||||
- Local path
|
||||
- Built-in SFTP
|
||||
- Doge Cloud
|
||||
- Huawei Cloud OBS
|
||||
- Alist
|
||||
- Lsky Pro
|
||||
|
||||
## 4. Is it possible to upload video files?
|
||||
|
||||
Yes. With the newly added image hosting management function, you can upload files of any format, including video files. At the same time, when uploading in the management interface, using methods such as chunked uploading and streaming uploading is faster and more stable compared to the base64 conversion method built into PicGo.
|
||||
|
||||
## 5. Does PicList support a certain image hosting platform?
|
||||
|
||||
PicList itself supports the following image hosting platforms:
|
||||
|
||||
- Qiniu Cloud
|
||||
- AWS S3 compatible platform
|
||||
- Tencent Cloud COS
|
||||
- Upyun
|
||||
- GitHub
|
||||
- SM.MS
|
||||
- Aliyun OSS
|
||||
- Imgur
|
||||
- Webdav
|
||||
- Local path
|
||||
- SFTP
|
||||
- Telegra.ph
|
||||
- Lsky Pro
|
||||
- PicList (nested)
|
||||
|
||||
PicList plans to integrate and optimize existing plugins and embed more commonly used image hosting platforms.
|
||||
|
||||
In addition, PicList is compatible with PicGo's plugin system. If you need support for other image hosting platforms, you can refer to the [PicGo third-party plugins](https://github.com/PicGo/Awesome-PicGo). If you still cannot find the image hosting platform you need, please develop a plugin for everyone to use.
|
||||
|
||||
## 6. The GitHub image hosting platform sometimes uploads successfully and sometimes fails
|
||||
|
||||
The problem with GitHub servers and China's Great Firewall may cause successful or failed uploads. There is no solution.
|
||||
|
||||
If you want stability, please use paid cloud storage such as Aliyun and Tencent Cloud. The price is not expensive.
|
||||
|
||||
## 7. The main window interface of PicList cannot be opened on Mac
|
||||
|
||||
To open the main window, right-click or two-finger tap the PicList icon in the top bar and select "Open Main Window."
|
||||
|
||||
Or right-click the PicList icon in the dock and select "Open Main Window."
|
||||
|
||||
## 8. 上传失败,或者是服务器出错
|
||||
|
||||
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. 通常网络问题引起的上传失败都是因为代理设置不当导致的。如果开启了系统代理,建议同时也在 PicList 的代理设置中设置对应的HTTP代理。
|
||||
4. PicList's built-in image hosting platforms have been tested, and upload errors are generally not caused by PicList itself. If you are using the GitHub image hosting platform, please refer to the 7th point above.
|
||||
5. Check PicList's log (the error log can be found by clicking "Open" in PicList Settings -> Set Log File ->), and see if there is any key information in the `[PicList Error]` error message
|
||||
|
||||
1. First search for the error message in the error message by yourself, and you can often Baidu or Google the cause of the problem, so you don't need to open an issue.
|
||||
2. If there are `401`, `403` and other `40X` status code words, don't doubt it, it means that your configuration is wrong, check the configuration carefully to see if there are extra spaces, etc.
|
||||
3. If there are words such as `HttpError`, `RequestError`, `socket hang up`, etc., it means that this is a network problem, and I cannot help you solve the network problem. Please check your own network, whether there is a proxy, whether the DNS setting is normal, etc.
|
||||
6. Usually, upload failures caused by network problems are caused by improper proxy settings. If the system proxy is turned on, it is recommended to set the corresponding HTTP proxy in the proxy settings of PicList at the same time.
|
||||
|
||||
## 10. After installing the macOS version, there is no main interface
|
||||
|
||||
Please find the icon of PicList in the top bar, and then right-click (touchpad two-finger tap, or right-click the mouse) to find the menu of "Open Main Window".
|
||||
|
||||
Or right-click on the icon of PicList in the Docker bar to find the menu of "Open Main Window".
|
||||
|
||||
## 11. After installing PicList on macOS, it shows "The file is damaged" or there is no response after installing and opening
|
||||
|
||||
Please upgrade PicList to version 1.4.1 or above. Starting from 1.4.1, PicList has been signed by Apple and will not have this problem again.
|
||||
|
||||
## 12. Watermark is not added normally
|
||||
|
||||
Before adding a watermark, PicList will check whether the font file exists. If it does not exist, it will automatically download the font file. However, due to network problems, the font file may fail to download, and the watermark will be skipped.
|
||||
|
||||
Please check whether the font file under the corresponding path according to your system exists. If it does not exist, please download the font file manually and put it in the corresponding path.
|
||||
|
||||
Windows: `%APPDATA%\piclist\assets\simhei.ttf`
|
||||
Linux: `$XDG_CONFIG_HOME/piclist/assets/simhei.ttf` or `~/.config//assets/simhei.ttf`
|
||||
MacOS: `~/Library/Application\ Support/picgo/assets/simhei.ttf`
|
||||
|
||||
The font file download address: [https://release.piclist.cn/simhei.ttf](https://release.piclist.cn/simhei.ttf)
|
||||
|
||||
## 13. Upload failed when using aws-s3 plugin to upload to cloudflare R2
|
||||
|
||||
R2's endpoint address will be blocked by GFW sni. After checking piclist.log, adding the corresponding IP address to the proxy list can solve the problem.
|
||||
|
||||
## 14. Are all PicGo plugins compatible with PicList?
|
||||
|
||||
PicList is compatible with most PicGo plugins. However, since PicList uses an updated version of electron, it is not compatible with the old version of the sharp library, so some plugins may not work.
|
||||
|
||||
Known plugins that cannot be used are:
|
||||
|
||||
- picgo-plugin-watermark (built-in)
|
||||
- picgo-plugin-pic-migrater (this plugin will verify the version of PicGo and cannot be used, please use the pic-migrater-piclist plugin)
|
||||
- picgo-plugin-auto-delete (built-in)
|
||||
|
||||
Welcome everyone to test other plugins. If there are plugins that cannot be used, please open an issue for feedback.
|
||||
|
||||
## 15. How to run PicList-core through Docker?
|
||||
|
||||
### docker run
|
||||
|
||||
Change the `./piclist` to your own path, and change the `piclist123456` to your own secret key.
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name piclist \
|
||||
--restart always \
|
||||
-p 36677:36677 \
|
||||
-v "./piclist:/root/.piclist" \
|
||||
kuingsmile/piclist:latest \
|
||||
node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
### docker-compose
|
||||
|
||||
download `docker-compose.yml` from piclist-core repo, or copy the following content to `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
node:
|
||||
image: 'kuingsmile/piclist:latest'
|
||||
container_name: piclist
|
||||
restart: always
|
||||
ports:
|
||||
- 36677:36677
|
||||
volumes:
|
||||
- './piclist:/root/.piclist'
|
||||
command: node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
You can change the `volumes` to your own path and change the `command` to your own secret key.
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
# FAQ
|
||||
|
||||
This FAQ has been modified from PicGo's FAQ, and thanks to the author Molunerfinn for PicGo.
|
||||
|
||||
## Common Questions
|
||||
|
||||
> Please refer to [user manual](https://piclist.cn/en) for general configuration issues.
|
||||
|
||||
## 1. What is the relationship between PicList and PicGo?
|
||||
|
||||
PicList is forked from the PicGo project and is based on PicGo for secondary development. At the same time, the core function kernel PicGo-Core has been developed for the second time and renamed as [PicList-Core](https://github.com/Kuingsmile/PicList-Core).
|
||||
|
||||
The addition of all new features to PicList has not affected PicGo's existing functions, so you can use all PicGo plugins in PicList. At the same time, it can still be used in conjunction with software such as Typora and Obsidian.
|
||||
|
||||
## 2. When using the image hosting management function, errors such as "unable to retrieve directory" occur
|
||||
|
||||
Please check the log file `manage.log`. In addition, the API calls of various platforms have a limit on the number of calls per hour. If an error occurs, please try again later.
|
||||
|
||||
## 3. Which remote image hosting deletion is supported?
|
||||
|
||||
Currently, the supported image hosting platforms are:
|
||||
|
||||
- Aliyun OSS
|
||||
- Tencent Cloud COS
|
||||
- Qiniu Cloud Kodo
|
||||
- Upyun
|
||||
- SM.MS
|
||||
- Imgur
|
||||
- GitHub
|
||||
- Webdav
|
||||
- Aws S3
|
||||
- Local path
|
||||
- Built-in SFTP
|
||||
- Doge Cloud
|
||||
- Huawei Cloud OBS
|
||||
- Alist
|
||||
- Lsky Pro
|
||||
|
||||
## 4. Is it possible to upload video files?
|
||||
|
||||
Yes. With the newly added image hosting management function, you can upload files of any format, including video files. At the same time, when uploading in the management interface, using methods such as chunked uploading and streaming uploading is faster and more stable compared to the base64 conversion method built into PicGo.
|
||||
|
||||
## 5. Does PicList support a certain image hosting platform?
|
||||
|
||||
PicList itself supports the following image hosting platforms:
|
||||
|
||||
- Qiniu Cloud
|
||||
- AWS S3 compatible platform
|
||||
- Tencent Cloud COS
|
||||
- Upyun
|
||||
- GitHub
|
||||
- SM.MS
|
||||
- Aliyun OSS
|
||||
- Imgur
|
||||
- Webdav
|
||||
- Local path
|
||||
- SFTP
|
||||
- Telegra.ph
|
||||
- Lsky Pro
|
||||
- PicList (nested)
|
||||
|
||||
PicList plans to integrate and optimize existing plugins and embed more commonly used image hosting platforms.
|
||||
|
||||
In addition, PicList is compatible with PicGo's plugin system. If you need support for other image hosting platforms, you can refer to the [PicGo third-party plugins](https://github.com/PicGo/Awesome-PicGo). If you still cannot find the image hosting platform you need, please develop a plugin for everyone to use.
|
||||
|
||||
## 6. The GitHub image hosting platform sometimes uploads successfully and sometimes fails
|
||||
|
||||
The problem with GitHub servers and China's Great Firewall may cause successful or failed uploads. There is no solution.
|
||||
|
||||
If you want stability, please use paid cloud storage such as Aliyun and Tencent Cloud. The price is not expensive.
|
||||
|
||||
## 7. The main window interface of PicList cannot be opened on Mac
|
||||
|
||||
To open the main window, right-click or two-finger tap the PicList icon in the top bar and select "Open Main Window."
|
||||
|
||||
Or right-click the PicList icon in the dock and select "Open Main Window."
|
||||
|
||||
## 8. Upload failed, or server error
|
||||
|
||||
1. PicList's built-in image hosting platforms have been tested, and upload errors are generally not caused by PicList itself. If you are using the GitHub image hosting platform, please refer to the 7th point above.
|
||||
2. Check PicList's log (the error log can be found by clicking "Open" in PicList Settings -> Set Log File ->), and see if there is any key information in the `[PicList Error]` error message
|
||||
|
||||
1. First search for the error message in the error message by yourself, and you can often Baidu or Google the cause of the problem, so you don't need to open an issue.
|
||||
2. If there are `401`, `403` and other `40X` status code words, don't doubt it, it means that your configuration is wrong, check the configuration carefully to see if there are extra spaces, etc.
|
||||
3. If there are words such as `HttpError`, `RequestError`, `socket hang up`, etc., it means that this is a network problem, and I cannot help you solve the network problem. Please check your own network, whether there is a proxy, whether the DNS setting is normal, etc.
|
||||
3. Usually, upload failures caused by network problems are caused by improper proxy settings. If the system proxy is turned on, it is recommended to set the corresponding HTTP proxy in the proxy settings of PicList at the same time.
|
||||
|
||||
## 10. After installing the macOS version, there is no main interface
|
||||
|
||||
Please find the icon of PicList in the top bar, and then right-click (touchpad two-finger tap, or right-click the mouse) to find the menu of "Open Main Window".
|
||||
|
||||
Or right-click on the icon of PicList in the Docker bar to find the menu of "Open Main Window".
|
||||
|
||||
## 11. After installing PicList on macOS, it shows "The file is damaged" or there is no response after installing and opening
|
||||
|
||||
Please upgrade PicList to version 1.4.1 or above. Starting from 1.4.1, PicList has been signed by Apple and will not have this problem again.
|
||||
|
||||
## 12. Watermark is not added normally
|
||||
|
||||
Before adding a watermark, PicList will check whether the font file exists. If it does not exist, it will automatically download the font file. However, due to network problems, the font file may fail to download, and the watermark will be skipped.
|
||||
|
||||
Please check whether the font file under the corresponding path according to your system exists. If it does not exist, please download the font file manually and put it in the corresponding path.
|
||||
|
||||
Windows: `%APPDATA%\piclist\assets\simhei.ttf`
|
||||
Linux: `$XDG_CONFIG_HOME/piclist/assets/simhei.ttf` or `~/.config//assets/simhei.ttf`
|
||||
MacOS: `~/Library/Application\ Support/picgo/assets/simhei.ttf`
|
||||
|
||||
The font file download address: [https://release.piclist.cn/simhei.ttf](https://release.piclist.cn/simhei.ttf)
|
||||
|
||||
## 13. Upload failed when using aws-s3 plugin to upload to cloudflare R2
|
||||
|
||||
R2's endpoint address will be blocked by GFW sni. After checking piclist.log, adding the corresponding IP address to the proxy list can solve the problem.
|
||||
|
||||
## 14. Are all PicGo plugins compatible with PicList?
|
||||
|
||||
PicList is compatible with most PicGo plugins. However, since PicList uses an updated version of electron, it is not compatible with the old version of the sharp library, so some plugins may not work.
|
||||
|
||||
Known plugins that cannot be used are:
|
||||
|
||||
- picgo-plugin-watermark (built-in)
|
||||
- picgo-plugin-pic-migrater (this plugin will verify the version of PicGo and cannot be used, please use the pic-migrater-piclist plugin)
|
||||
- picgo-plugin-auto-delete (built-in)
|
||||
|
||||
Welcome everyone to test other plugins. If there are plugins that cannot be used, please open an issue for feedback.
|
||||
|
||||
## 15. How to run PicList-core through Docker?
|
||||
|
||||
### docker run
|
||||
|
||||
Change the `./piclist` to your own path, and change the `piclist123456` to your own secret key.
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name piclist \
|
||||
--restart always \
|
||||
-p 36677:36677 \
|
||||
-v "./piclist:/root/.piclist" \
|
||||
kuingsmile/piclist:latest \
|
||||
node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
### docker-compose
|
||||
|
||||
download `docker-compose.yml` from piclist-core repo, or copy the following content to `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
node:
|
||||
image: 'kuingsmile/piclist:latest'
|
||||
container_name: piclist
|
||||
restart: always
|
||||
ports:
|
||||
- 36677:36677
|
||||
volumes:
|
||||
- './piclist:/root/.piclist'
|
||||
command: node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
You can change the `volumes` to your own path and change the `command` to your own secret key.
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
@@ -1,254 +1,254 @@
|
||||
<div align="center">
|
||||
<img src="https://imgx.horosama.com/admin_uploads/2022/10/2022_10_05_633d79e401694.png" alt="">
|
||||
<h1>PicList</h1>
|
||||
<a href="https://github.com/Kuingsmile/PicList/actions">
|
||||
<img src="https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/releases">
|
||||
<img src="https://img.shields.io/github/downloads/Kuingsmile/PicList/total.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/actions">
|
||||
<img src="https://github.com/Kuingsmile/PicList/actions/workflows/manually.yml/badge.svg" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/releases/latest">
|
||||
<img src="https://img.shields.io/github/release/Kuingsmile/PicList.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
[简体中文](https://github.com/Kuingsmile/PicList/blob/dev/README_cn.md) | English
|
||||
|
||||
PicList is an efficient cloud storage and image hosting platform management tool. Building upon the foundation of PicGo, it has been deeply modified and enhanced. Not only does it retain all of PicGo's features, but it also adds many new ones. For instance, the album now supports synchronized deletion of files in the cloud. Built-in image hosting options have been expanded to include WebDav, local image hosting, and SFTP. Additionally, PicList introduces comprehensive cloud storage management functions, including cloud directory viewing, file search, batch uploading, downloading, and file deletion, copying links in various formats, and previews for images, markdown, text, and videos. Moreover, it boasts a more powerful album function and numerous other improvements and enhancements.
|
||||
|
||||
## How to migrate from PicGo
|
||||
|
||||
PicList `V1.5.0` and above versions provide the `one-click migration` function. Enter the `Settings` page, and then click the right button on the `Migrate from PicGo` option to migrate. Please restart the application to take effect after migration.
|
||||
|
||||
## PicList-Core
|
||||
|
||||
The core of PicList is based on the original PicGo-Core [PicList-core](https://github.com/Kuingsmile/PicList-Core), which is adapted for cloud deletion and other functions, and adds watermark addition and image compression / scaling / rotation / format conversion functions, which can be called through CLI command line, support starting the built-in upload server through `picgo-server`,as well as some other function changes.
|
||||
|
||||
if you want to use PicList-core, please go to [https://github.com/Kuingsmile/PicList-Core](https://github.com/Kuingsmile/PicList-Core), or go to [npm official address](https://www.npmjs.com/package/piclist) to view the installation instructions.
|
||||
|
||||
## Features
|
||||
|
||||
- Retains all the features of PicGo and is compatible with the vast majority of existing PicGo plugins, including integrations with software like Typora and Obsidian.
|
||||
- Added multiple built-in image hosting platforms, such as WebDav, Lsky Pro, local image hosting, SFTP and Telegra.ph. The original built-in imgur image host now also supports account login for uploading.
|
||||
- Within the album, you can synchronize the deletion of cloud images. This is supported across all built-in image hosts and multiple plugins.
|
||||
- The album now offers advanced search and sorting features, as well as batch URL modification.
|
||||
- Built-in tools for adding watermarks, compressing images, scaling images, rotating images, and converting image formats are now available. Advanced renaming is also supported.
|
||||
- Upload interface supports form upload files, can be shared by multiple computers
|
||||
- Configuration can be synchronized to Github/Gitee/Gitea repositories.
|
||||
- Manages over ten types of image hosting platforms, allowing online viewing of cloud directories, file searching, batch uploading, batch downloading, file deletion, and more.
|
||||
- Support previewing multiple formats of files, including pictures, videos, plain text files and markdown files, etc. For the specific formats supported, please refer to [Supported file format list](https://github.com/Kuingsmile/PicList/blob/dev/supported_format.md)
|
||||
- Supports the use of regular expressions for batch renaming of cloud files.
|
||||
- For private storage buckets, pre-signed link copying for sharing is available.
|
||||
- Software auto-updates are available, along with multiple startup modes, and many other feature details have been added and optimized.
|
||||
- The PicGo interface has been enhanced, window size restrictions have been unlocked, and some interface layouts have been beautified.
|
||||
- The installation package for the Mac platform is now signed, addressing the recurring issue on PicGo where the installation package was reported as damaged.
|
||||
|
||||
## How to use
|
||||
|
||||
### How to use in VScode
|
||||
|
||||
Please install my matching plugin [VS-PicList](https://marketplace.visualstudio.com/items?itemName=Kuingsmile.vs-piclist), compared with the vs-picgo plugin, the plugin directly depends on the PicList desktop software, supports a variety of uploads and direct cloud deletion in Vscode and other functions.
|
||||
|
||||
### How to use in Typora
|
||||
|
||||
#### **Version 1.6.0-dev and above**
|
||||
|
||||
**Typora 1.6.0-dev and above versions now support PicList natively**
|
||||
|
||||
Now you need to set the upload service PicList and PicGo (app) to the installation path of PicList at the same time, it has been fixed in latest dev version.
|
||||
|
||||
[download link](https://typora.io/releases/all)
|
||||
|
||||
#### **Version < 1.6.0-dev**
|
||||
|
||||
Windows:
|
||||
|
||||
Enter the Typora settings page, select the image, set the upload service to `PicGo(app)`, and then fill in the installation path of PicList in `PicGo path`, as shown below:
|
||||
|
||||

|
||||
|
||||
Or, you can also use the `npm install piclist` command to install PicList-core, and then set the upload service to `PicGo-Core(command line)`.
|
||||
|
||||
MacOS:
|
||||
|
||||
Enter the Typora settings page, select the image, set the upload service to `Custom Command`, and then fill in `/Applications/PicList.app/Contents/MacOS/PicList upload` in `Command`, as shown below:
|
||||
|
||||

|
||||
|
||||
The verification of the upload option may have problems, you can ignore it, and use it directly.
|
||||
|
||||
### How to use in Obsidian
|
||||
|
||||
In the community plugins, search for and install the Image auto upload Plugin. Next, go to the plugin settings page and change the default uploader to PicGo(app). Set the PicGo server to `http://127.0.0.1:36677/upload` as shown in the image below. Additionally, this plugin also supports cloud-based deletion through PicList. To use this feature, enter `http://127.0.0.1:36677/delete` in the deletion interface.
|
||||
|
||||

|
||||
|
||||
### How to use in docker
|
||||
|
||||
#### docker run
|
||||
|
||||
Change the `./piclist` to your own path, this is the path where you put your `config.json` file, and change the `piclist123456` to your own secret key.
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name piclist \
|
||||
--restart always \
|
||||
-p 36677:36677 \
|
||||
-v "./piclist:/root/.piclist" \
|
||||
kuingsmile/piclist:latest \
|
||||
node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
#### docker-compose
|
||||
|
||||
download `docker-compose.yml` from piclist-core repo, or copy the following content to `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
node:
|
||||
image: 'kuingsmile/piclist:latest'
|
||||
container_name: piclist
|
||||
restart: always
|
||||
ports:
|
||||
- 36677:36677
|
||||
volumes:
|
||||
- './piclist:/root/.piclist'
|
||||
command: node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
You can change the `./piclist` to your own path, this is the path where you put your `config.json` file, and change the `command` to your own secret key.
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Supported platforms
|
||||
|
||||
| Platform | Album cloud deletion | Cloud storage management |
|
||||
| :------------------------: | :------------------: | :----------------------: |
|
||||
| SM.MS | ✔️ | ✔️ |
|
||||
| Github | ✔️ | ✔️ |
|
||||
| Imgur | ✔️ | ✔️ |
|
||||
| Tencent COS V5 | ✔️ | ✔️ |
|
||||
| Aliyun OSS | ✔️ | ✔️ |
|
||||
| Upyun | ✔️ | ✔️ |
|
||||
| Qiniu | ✔️ | ✔️ |
|
||||
| S3 API compatible platform | ✔️ | ✔️ |
|
||||
| WebDAV | ✔️ | ✔️ |
|
||||
| Local | ✔️ | ✔️ |
|
||||
| Built-in SFTP | ✔️ | ✔️ |
|
||||
| Doge Cloud | ✔️ | ✔️ |
|
||||
| PicList(Lasso-Doll) | ✔️ | ✔️ |
|
||||
| Telegra.ph | ✔️ | × |
|
||||
| Lsky Pro | ✔️ | ✔️ |
|
||||
|
||||
| Plugin | Album cloud deletion |
|
||||
| :----------------------------------------------------------------------------------------: | :------------------: |
|
||||
| [picgo-plugin-s3](https://github.com/wayjam/picgo-plugin-s3) | ✔️ |
|
||||
| [picgo-plugin-alist](https://github.com/jinzhi0123/picgo-plugin-alist) | ✔️ |
|
||||
| [picgo-plugin-huawei-uploader](https://github.com/YunfengGao/picgo-plugin-huawei-uploader) | ✔️ |
|
||||
| [picgo-plugin-dogecloud](https://github.com/w4j1e/picgo-plugin-dogecloud) | ✔️ |
|
||||
|
||||
## Download and install
|
||||
|
||||
[https://github.com/Kuingsmile/PicList/releases/latest](https://github.com/Kuingsmile/PicList/releases/latest)
|
||||
|
||||
### Homebrew
|
||||
|
||||
MacOS users can now use Homebrew to install PicList, just execute the following command:
|
||||
|
||||
```bash
|
||||
brew install piclist --cask
|
||||
```
|
||||
|
||||
Uninstall:
|
||||
|
||||
```bash
|
||||
brew uninstall piclist
|
||||
```
|
||||
|
||||
### Mac special instructions
|
||||
|
||||
If the macOS system installs PicList and displays "file is damaged" or installs and opens without response, please upgrade to PicList V1.4.1 or above.
|
||||
|
||||
After V1.4.1, all mac installation packages have been signed by my developer certificate, will not be recognized by the macOS system as "malicious software", will not appear "file is damaged" prompt.
|
||||
|
||||
### Mac App Store
|
||||
|
||||
Due to the sandbox mechanism of Mac App Store, many functions cannot be used normally, so the installation method of Mac App Store is no longer supported.
|
||||
|
||||
If you have purchased PicList through Mac App Store, please add my WeChat `pku_sq_ma`, I will refund you.
|
||||
|
||||
Thank you again for your support for PicList.
|
||||
|
||||
**Of course, if you don't want to pay, you can also download the free installation package through Github to install.**
|
||||
|
||||
## Application screenshot
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Development instructions
|
||||
|
||||
1. You need to have Node, Git environment, and understand the related knowledge of npm.
|
||||
2. git clone [https://github.com/Kuingsmile/PicList.git](https://github.com/Kuingsmile/PicList.git) and enter the project.
|
||||
`yarn` download dependencies
|
||||
Note that if you don't have `yarn`, please go to the official website to download and install it before using it. Using `npm install` will cause unknown errors!
|
||||
3. Mac needs `Xcode` environment, Windows needs `VS` environment.
|
||||
4. If you need to contribute code, you can refer to [contribution guide](https://github.com/Kuingsmile/PicList/blob/dev/CONTRIBUTING_EN.md)
|
||||
|
||||
### Development mode
|
||||
|
||||
Enter `yarn run dev` to enter development mode, which has hot reload feature. However, it should be noted that the development mode is unstable and there will be process crashes. At this time, you need to:
|
||||
|
||||
`ctrl+c` # Exit development mode
|
||||
`yarn run dev` # Re-enter development mode
|
||||
|
||||
Note: After the development mode is running, the application icon of PicList will appear in the application area of the taskbar in the lower right corner of the bottom bar.
|
||||
|
||||
### Production mode
|
||||
|
||||
If you need to build it yourself, you can start building with `yarn run build`. After the build is successful, the corresponding installation file will appear in the `dist_electron` directory.
|
||||
|
||||
## Other related
|
||||
|
||||
- [PicList-Core](https://github.com/Kuingsmile/PicList-Core) : A core library based on PicGo-Core for CLI operations and project development
|
||||
- [PicHoro](https://github.com/Kuingsmile/PicHoro): A mobile APP that works with PicList
|
||||
- [VS-PicList](https://github.com/Kuingsmile/vs-PicList/): A VScode plugin that works with PicList
|
||||
|
||||
## Communication group
|
||||
|
||||
If you have any questions, you can join the TG group for communication.
|
||||
|
||||
[PicList TG Group](https://t.me/+rq8y7wsj7Pg5ZTg1)
|
||||
|
||||

|
||||
|
||||
## License
|
||||
|
||||
This project is open source under the MIT license. Welcome everyone to use and contribute code. Thank you for the open source spirit of the original author Molunerfinn.
|
||||
|
||||
[MIT](https://opensource.org/licenses/MIT)
|
||||
|
||||
Copyright (c) 2017-present Molunerfinn
|
||||
|
||||
Copyright (c) 2023-present Kuingsmile
|
||||
|
||||
## Star Me
|
||||
|
||||
- Star [](https://github.com/kuingsmile/PicList/stargazers)
|
||||
|
||||
[](https://github.com/kuingsmile/PicList/stargazers)
|
||||
<div align="center">
|
||||
<img src="https://imgx.horosama.com/admin_uploads/2022/10/2022_10_05_633d79e401694.png" alt="">
|
||||
<h1>PicList</h1>
|
||||
<a href="https://github.com/Kuingsmile/PicList/actions">
|
||||
<img src="https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/releases">
|
||||
<img src="https://img.shields.io/github/downloads/Kuingsmile/PicList/total.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/actions">
|
||||
<img src="https://github.com/Kuingsmile/PicList/actions/workflows/manually.yml/badge.svg" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/releases/latest">
|
||||
<img src="https://img.shields.io/github/release/Kuingsmile/PicList.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
[简体中文](https://github.com/Kuingsmile/PicList/blob/dev/README_cn.md) | English
|
||||
|
||||
PicList is an efficient cloud storage and image hosting platform management tool. Building upon the foundation of PicGo, it has been deeply modified and enhanced. Not only does it retain all of PicGo's features, but it also adds many new ones. For instance, the album now supports synchronized deletion of files in the cloud. Built-in image hosting options have been expanded to include WebDav, local image hosting, and SFTP. Additionally, PicList introduces comprehensive cloud storage management functions, including cloud directory viewing, file search, batch uploading, downloading, and file deletion, copying links in various formats, and previews for images, markdown, text, and videos. Moreover, it boasts a more powerful album function and numerous other improvements and enhancements.
|
||||
|
||||
## How to migrate from PicGo
|
||||
|
||||
PicList `V1.5.0` and above versions provide the `one-click migration` function. Enter the `Settings` page, and then click the right button on the `Migrate from PicGo` option to migrate. Please restart the application to take effect after migration.
|
||||
|
||||
## PicList-Core
|
||||
|
||||
The core of PicList is based on the original PicGo-Core [PicList-core](https://github.com/Kuingsmile/PicList-Core), which is adapted for cloud deletion and other functions, and adds watermark addition and image compression / scaling / rotation / format conversion functions, which can be called through CLI command line, support starting the built-in upload server through `picgo-server`,as well as some other function changes.
|
||||
|
||||
if you want to use PicList-core, please go to [https://github.com/Kuingsmile/PicList-Core](https://github.com/Kuingsmile/PicList-Core), or go to [npm official address](https://www.npmjs.com/package/piclist) to view the installation instructions.
|
||||
|
||||
## Features
|
||||
|
||||
- Retains all the features of PicGo and is compatible with the vast majority of existing PicGo plugins, including integrations with software like Typora and Obsidian.
|
||||
- Added multiple built-in image hosting platforms, such as WebDav, Lsky Pro, local image hosting, SFTP and Telegra.ph. The original built-in imgur image host now also supports account login for uploading.
|
||||
- Within the album, you can synchronize the deletion of cloud images. This is supported across all built-in image hosts and multiple plugins.
|
||||
- The album now offers advanced search and sorting features, as well as batch URL modification.
|
||||
- Built-in tools for adding watermarks, compressing images, scaling images, rotating images, and converting image formats are now available. Advanced renaming is also supported.
|
||||
- Upload interface supports form upload files, can be shared by multiple computers
|
||||
- Configuration can be synchronized to Github/Gitee/Gitea repositories.
|
||||
- Manages over ten types of image hosting platforms, allowing online viewing of cloud directories, file searching, batch uploading, batch downloading, file deletion, and more.
|
||||
- Support previewing multiple formats of files, including pictures, videos, plain text files and markdown files, etc. For the specific formats supported, please refer to [Supported file format list](https://github.com/Kuingsmile/PicList/blob/dev/supported_format.md)
|
||||
- Supports the use of regular expressions for batch renaming of cloud files.
|
||||
- For private storage buckets, pre-signed link copying for sharing is available.
|
||||
- Software auto-updates are available, along with multiple startup modes, and many other feature details have been added and optimized.
|
||||
- The PicGo interface has been enhanced, window size restrictions have been unlocked, and some interface layouts have been beautified.
|
||||
- The installation package for the Mac platform is now signed, addressing the recurring issue on PicGo where the installation package was reported as damaged.
|
||||
|
||||
## How to use
|
||||
|
||||
### How to use in VScode
|
||||
|
||||
Please install my matching plugin [VS-PicList](https://marketplace.visualstudio.com/items?itemName=Kuingsmile.vs-piclist), compared with the vs-picgo plugin, the plugin directly depends on the PicList desktop software, supports a variety of uploads and direct cloud deletion in Vscode and other functions.
|
||||
|
||||
### How to use in Typora
|
||||
|
||||
#### **Version 1.6.0-dev and above**
|
||||
|
||||
**Typora 1.6.0-dev and above versions now support PicList natively**
|
||||
|
||||
Now you need to set the upload service PicList and PicGo (app) to the installation path of PicList at the same time, it has been fixed in latest dev version.
|
||||
|
||||
[download link](https://typora.io/releases/all)
|
||||
|
||||
#### **Version < 1.6.0-dev**
|
||||
|
||||
Windows:
|
||||
|
||||
Enter the Typora settings page, select the image, set the upload service to `PicGo(app)`, and then fill in the installation path of PicList in `PicGo path`, as shown below:
|
||||
|
||||

|
||||
|
||||
Or, you can also use the `npm install piclist` command to install PicList-core, and then set the upload service to `PicGo-Core(command line)`.
|
||||
|
||||
MacOS:
|
||||
|
||||
Enter the Typora settings page, select the image, set the upload service to `Custom Command`, and then fill in `/Applications/PicList.app/Contents/MacOS/PicList upload` in `Command`, as shown below:
|
||||
|
||||

|
||||
|
||||
The verification of the upload option may have problems, you can ignore it, and use it directly.
|
||||
|
||||
### How to use in Obsidian
|
||||
|
||||
In the community plugins, search for and install the Image auto upload Plugin. Next, go to the plugin settings page and change the default uploader to PicGo(app). Set the PicGo server to `http://127.0.0.1:36677/upload` as shown in the image below. Additionally, this plugin also supports cloud-based deletion through PicList. To use this feature, enter `http://127.0.0.1:36677/delete` in the deletion interface.
|
||||
|
||||

|
||||
|
||||
### How to use in docker
|
||||
|
||||
#### docker run
|
||||
|
||||
Change the `./piclist` to your own path, this is the path where you put your `config.json` file, and change the `piclist123456` to your own secret key.
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name piclist \
|
||||
--restart always \
|
||||
-p 36677:36677 \
|
||||
-v "./piclist:/root/.piclist" \
|
||||
kuingsmile/piclist:latest \
|
||||
node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
#### docker-compose
|
||||
|
||||
download `docker-compose.yml` from piclist-core repo, or copy the following content to `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
node:
|
||||
image: 'kuingsmile/piclist:latest'
|
||||
container_name: piclist
|
||||
restart: always
|
||||
ports:
|
||||
- 36677:36677
|
||||
volumes:
|
||||
- './piclist:/root/.piclist'
|
||||
command: node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
You can change the `./piclist` to your own path, this is the path where you put your `config.json` file, and change the `command` to your own secret key.
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Supported platforms
|
||||
|
||||
| Platform | Album cloud deletion | Cloud storage management |
|
||||
| :------------------------: | :------------------: | :----------------------: |
|
||||
| Built-in AList | ✔️ | ✔️ |
|
||||
| SM.MS | ✔️ | ✔️ |
|
||||
| Github | ✔️ | ✔️ |
|
||||
| Imgur | ✔️ | ✔️ |
|
||||
| Tencent COS V5 | ✔️ | ✔️ |
|
||||
| Aliyun OSS | ✔️ | ✔️ |
|
||||
| Upyun | ✔️ | ✔️ |
|
||||
| Qiniu | ✔️ | ✔️ |
|
||||
| S3 API compatible platform | ✔️ | ✔️ |
|
||||
| WebDAV | ✔️ | ✔️ |
|
||||
| Local | ✔️ | ✔️ |
|
||||
| Built-in SFTP | ✔️ | ✔️ |
|
||||
| Doge Cloud | ✔️ | ✔️ |
|
||||
| PicList(Lasso-Doll) | ✔️ | ✔️ |
|
||||
| Telegra.ph | ✔️ | × |
|
||||
| Lsky Pro | ✔️ | ✔️ |
|
||||
|
||||
| Plugin | Album cloud deletion |
|
||||
| :----------------------------------------------------------------------------------------: | :------------------: |
|
||||
| [picgo-plugin-s3](https://github.com/wayjam/picgo-plugin-s3) | ✔️ |
|
||||
| [picgo-plugin-alist](https://github.com/jinzhi0123/picgo-plugin-alist) | ✔️ |
|
||||
| [picgo-plugin-huawei-uploader](https://github.com/YunfengGao/picgo-plugin-huawei-uploader) | ✔️ |
|
||||
| [picgo-plugin-dogecloud](https://github.com/w4j1e/picgo-plugin-dogecloud) | ✔️ |
|
||||
|
||||
## Download and install
|
||||
|
||||
[https://github.com/Kuingsmile/PicList/releases/latest](https://github.com/Kuingsmile/PicList/releases/latest)
|
||||
|
||||
### Scoop
|
||||
|
||||
Thanks to [scoop-lemon](https://github.com/hoilc/scoop-lemon), you can use Scoop to install PicList, just execute the following command:
|
||||
|
||||
```bash
|
||||
scoop bucket add lemon https://github.com/hoilc/scoop-lemon
|
||||
scoop install lemon/piclist
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
|
||||
MacOS users can now use Homebrew to install PicList, just execute the following command:
|
||||
|
||||
```bash
|
||||
brew install piclist --cask
|
||||
```
|
||||
|
||||
Uninstall:
|
||||
|
||||
```bash
|
||||
brew uninstall piclist
|
||||
```
|
||||
|
||||
### Mac special instructions
|
||||
|
||||
If the macOS system installs PicList and displays "file is damaged" or installs and opens without response, please upgrade to PicList V1.4.1 or above.
|
||||
|
||||
After V1.4.1, all mac installation packages have been signed by my developer certificate, will not be recognized by the macOS system as "malicious software", will not appear "file is damaged" prompt.
|
||||
|
||||
## Application screenshot
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Development instructions
|
||||
|
||||
1. You need to have Node, Git environment, and understand the related knowledge of npm.
|
||||
2. git clone [https://github.com/Kuingsmile/PicList.git](https://github.com/Kuingsmile/PicList.git) and enter the project.
|
||||
`yarn` download dependencies
|
||||
Note that if you don't have `yarn`, please go to the official website to download and install it before using it. Using `npm install` will cause unknown errors!
|
||||
3. Mac needs `Xcode` environment, Windows needs `VS` environment.
|
||||
4. If you need to contribute code, you can refer to [contribution guide](https://github.com/Kuingsmile/PicList/blob/dev/CONTRIBUTING_EN.md)
|
||||
|
||||
### Development mode
|
||||
|
||||
Enter `yarn run dev` to enter development mode, which has hot reload feature. However, it should be noted that the development mode is unstable and there will be process crashes. At this time, you need to:
|
||||
|
||||
`ctrl+c` # Exit development mode
|
||||
`yarn run dev` # Re-enter development mode
|
||||
|
||||
Note: After the development mode is running, the application icon of PicList will appear in the application area of the taskbar in the lower right corner of the bottom bar.
|
||||
|
||||
### Production mode
|
||||
|
||||
If you need to build it yourself, you can start building with `yarn run build`. After the build is successful, the corresponding installation file will appear in the `dist_electron` directory.
|
||||
|
||||
## Other related
|
||||
|
||||
- [PicList-Core](https://github.com/Kuingsmile/PicList-Core) : A core library based on PicGo-Core for CLI operations and project development
|
||||
- [PicHoro](https://github.com/Kuingsmile/PicHoro): A mobile APP that works with PicList
|
||||
- [VS-PicList](https://github.com/Kuingsmile/vs-PicList/): A VScode plugin that works with PicList
|
||||
|
||||
## Communication group
|
||||
|
||||
If you have any questions, you can join the TG group for communication.
|
||||
|
||||
[PicList TG Group](https://t.me/+rq8y7wsj7Pg5ZTg1)
|
||||
|
||||

|
||||
|
||||
## License
|
||||
|
||||
This project is open source under the MIT license. Welcome everyone to use and contribute code. Thank you for the open source spirit of the original author Molunerfinn.
|
||||
|
||||
[MIT](https://opensource.org/licenses/MIT)
|
||||
|
||||
Copyright (c) 2017-present Molunerfinn
|
||||
|
||||
Copyright (c) 2023-present Kuingsmile
|
||||
|
||||
## Star Me
|
||||
|
||||
- Star [](https://github.com/kuingsmile/PicList/stargazers)
|
||||
|
||||
[](https://github.com/kuingsmile/PicList/stargazers)
|
||||
|
||||
+260
-244
@@ -1,244 +1,260 @@
|
||||
<div align="center">
|
||||
<img src="https://imgx.horosama.com/admin_uploads/2022/10/2022_10_05_633d79e401694.png" alt="">
|
||||
<h1>PicList</h1>
|
||||
<a href="https://github.com/Kuingsmile/PicList/actions">
|
||||
<img src="https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/releases">
|
||||
<img src="https://img.shields.io/github/downloads/Kuingsmile/PicList/total.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/actions">
|
||||
<img src="https://github.com/Kuingsmile/PicList/actions/workflows/manually.yml/badge.svg" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/releases/latest">
|
||||
<img src="https://img.shields.io/github/release/Kuingsmile/PicList.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
简体中文 | [English](https://github.com/Kuingsmile/PicList/blob/dev/README.md)
|
||||
|
||||
PicList是一款高效的云存储和图床平台管理工具,在PicGo的基础上经过深度的二次开发,不仅完整保留了PicGo的所有功能,还增添了许多新的feature。例如相册支持同步云端删除文件,内置图床额外添加了WebDav、本地图床和SFTP等。PicList同时增加了完整的云存储管理功能,包括云端目录查看、文件搜索、批量上传下载和删除文件,复制多种格式文件链接和图片/markdown/文本/视频预览等,另外还有更加强大的相册和多项功能新增或优化。
|
||||
|
||||
## 如何从PicGo迁移
|
||||
|
||||
PicList `V1.5.0`以上版本提供 `一键迁移`功能,进入 `设置`页面,然后在 `从PicGo迁移`选项点击右侧按钮即可,迁移后请重启应用生效。
|
||||
|
||||
## PicList-Core
|
||||
|
||||
PicList的内核使用的是原版PicGo-Core基础上修改的[PicList-core](https://github.com/Kuingsmile/PicList-Core),为云端删除等功能做了适配,同时,新增了水印添加和图片压缩/缩放/旋转/格式转换等功能,可以通过CLI命令行调用,支持通过`picgo-server`命令开启上传服务器,还有一些其他的功能改动。
|
||||
|
||||
如果您希望使用PicList-core,请前往[https://github.com/Kuingsmile/PicList-Core](https://github.com/Kuingsmile/PicList-Core),或者前往[npm官方地址](https://www.npmjs.com/package/piclist)查看安装说明。
|
||||
|
||||
## 特色功能
|
||||
|
||||
- 保留了PicGo的所有功能,兼容绝大部分已有的PicGo插件,包括和Typora、Obsidian等软件的搭配
|
||||
- 新增了多个内置图床,如WebDav、兰空图床、本地图床、SFTP和Telegra.ph等,原内置imgur图床额外支持登录账号上传
|
||||
- 相册中可同步删除云端图片,支持所有内置图床和多个插件
|
||||
- 相册新增了高级搜索和排序,批量修改URL等功能
|
||||
- 内置水印添加、图片压缩、图片缩放、图片旋转和图片格式转换等功能,同时支持高级重命名
|
||||
- 上传接口支持表单上传文件,可多电脑共用
|
||||
- 支持配置同步至Github/Gitee/Gitea仓库
|
||||
- 支持管理十余种图床,可以在线进行云端目录查看、文件搜索、批量上传、批量下载、删除文件等
|
||||
- 支持预览多种格式的文件,包括图片、视频、纯文本文件和markdown文件等,具体支持的格式请参考[支持的文件格式列表](https://github.com/Kuingsmile/PicList/blob/dev/supported_format.md)
|
||||
- 支持启用正则表达式的批量云端文件重命名
|
||||
- 对于私有存储桶等支持复制预签名链接进行分享
|
||||
- 支持软件自动更新,支持多种启动模式,还有更多功能细节新增和优化
|
||||
- 优化了PicGo的界面,解锁了窗口大小限制,同时美化了部分界面布局
|
||||
- mac平台安装包已签名,从源头解决了PicGo上的安装包已损坏的日经问题
|
||||
|
||||
## 如何使用
|
||||
|
||||
### 如何在Vscode中使用
|
||||
|
||||
请安装我的配套插件 [VS-PicList](https://marketplace.visualstudio.com/items?itemName=Kuingsmile.vs-piclist),相比于vs-picgo插件,该插件直接依赖于PicList桌面端软件,支持多样上传和直接在Vscode中进行云端删除等功能。
|
||||
|
||||
### 如何在Typora中使用
|
||||
|
||||
#### **1.6.0版本及以上**
|
||||
|
||||
**Typora 1.6.0-dev以及以上版本现在已经原生支持PicList了**
|
||||
|
||||
目前Typora中需要同时设置上传服务PicList和PicGo(app)的路径为PicList的安装路径,已经在最新的Dev版本中修复。
|
||||
|
||||
[下载地址](https://typora.io/releases/all)
|
||||
|
||||
#### 1.6.0版本以下
|
||||
|
||||
Windows:
|
||||
|
||||
进入Typora设置界面,选择图像,将上传服务设置为 `PicGo(app)`,然后在 `PicGo路径`中填写PicList的安装路径,如下图所示:
|
||||
|
||||

|
||||
|
||||
或者,您也可以使用 `npm install piclist`命令安装PicList-core,然后上传服务设置为 `PicGo-Core(command line)`。
|
||||
|
||||
MacOS:
|
||||
|
||||
进入Typora设置界面,选择图像,将上传服务设置为 `Custom Command`,然后在 `Command`中填写 `/Applications/PicList.app/Contents/MacOS/PicList upload`,如下图所示:
|
||||
|
||||

|
||||
|
||||
验证上传选项可能会出现问题,可以忽略,直接使用是正常的。
|
||||
|
||||
### 如何在Obsidian中使用
|
||||
|
||||
在社区插件中搜索安装 `Image auto upload Plugin`,然后进入插件设置页面,修改默认上传器为 `PicGo(app)`,设置 `PicGo server`为 `http://127.0.0.1:36677/upload`即可,如下图所示, 此外该插件还额外支持通过PicList进行云端删除,请在删除接口内填入 `http://127.0.0.1:36677/delete`:
|
||||
|
||||

|
||||
|
||||
### 如何在Docker中使用
|
||||
|
||||
#### docker run
|
||||
|
||||
修改`./piclist`为你自己的配置文件`config.json`所在的路径,修改`piclist123456`为你自己的密钥。
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name piclist \
|
||||
--restart always \
|
||||
-p 36677:36677 \
|
||||
-v "./piclist:/root/.piclist" \
|
||||
kuingsmile/piclist:latest \
|
||||
node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
#### docker-compose
|
||||
|
||||
从piclist-core仓库下载`docker-compose.yml`文件,或者复制以下内容到`docker-compose.yml`文件中:
|
||||
|
||||
```yaml
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
node:
|
||||
image: 'kuingsmile/piclist:latest'
|
||||
container_name: piclist
|
||||
restart: always
|
||||
ports:
|
||||
- 36677:36677
|
||||
volumes:
|
||||
- './piclist:/root/.piclist'
|
||||
command: node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
你可以修改`./piclist`为你自己的配置文件`config.json`所在的路径,修改`command`为你自己的密钥。
|
||||
|
||||
然后运行
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 已支持平台
|
||||
|
||||
| 平台 | 相册云删除 | 云存储管理 |
|
||||
| :------------: | :--------: | :--------: |
|
||||
| SM.MS | ✔️ | ✔️ |
|
||||
| Github | ✔️ | ✔️ |
|
||||
| Imgur | ✔️ | ✔️ |
|
||||
| 腾讯COS V5 | ✔️ | ✔️ |
|
||||
| 阿里云OSS | ✔️ | ✔️ |
|
||||
| 又拍云 | ✔️ | ✔️ |
|
||||
| 七牛云 | ✔️ | ✔️ |
|
||||
| S3 API兼容平台 | ✔️ | ✔️ |
|
||||
| WebDAV | ✔️ | ✔️ |
|
||||
| 本地文件夹 | ✔️ | ✔️ |
|
||||
| 内置SFTP | ✔️ | ✔️ |
|
||||
| 多吉云 | ✔️ | ✔️ |
|
||||
| PicList(套娃) | ✔️ | ✔️ |
|
||||
| Telegra.ph | ✔️ | x |
|
||||
| 兰空图床 | ✔️ | ✔️ |
|
||||
|
||||
| 插件 | 相册云删除 |
|
||||
| :----------------------------------------------------------------------------------------: | :--------: |
|
||||
| [picgo-plugin-s3](https://github.com/wayjam/picgo-plugin-s3) | ✔️ |
|
||||
| [picgo-plugin-alist](https://github.com/jinzhi0123/picgo-plugin-alist) | ✔️ |
|
||||
| [picgo-plugin-huawei-uploader](https://github.com/YunfengGao/picgo-plugin-huawei-uploader) | ✔️ |
|
||||
| [picgo-plugin-dogecloud](https://github.com/w4j1e/picgo-plugin-dogecloud) | ✔️ |
|
||||
|
||||
## 下载安装
|
||||
|
||||
[https://github.com/Kuingsmile/PicList/releases/latest](https://github.com/Kuingsmile/PicList/releases/latest)
|
||||
|
||||
### Homebrew
|
||||
|
||||
MacOS用户现在可以使用Homebrew来安装PicList了,只需要执行以下命令即可:
|
||||
|
||||
```bash
|
||||
brew install piclist --cask
|
||||
```
|
||||
|
||||
卸载命令:
|
||||
|
||||
```bash
|
||||
brew uninstall piclist
|
||||
```
|
||||
|
||||
## 应用截图
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 开发说明
|
||||
|
||||
1. 你需要有 Node、Git 环境,了解 npm 的相关知识。
|
||||
2. git clone [https://github.com/Kuingsmile/PicList.git](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)。
|
||||
|
||||
### 开发模式
|
||||
|
||||
输入 `yarn run dev` 进入开发模式,开发模式具有热重载特性。不过需要注意的是,开发模式不稳定,会有进程崩溃的情况。此时需要:
|
||||
|
||||
`ctrl+c` # 退出开发模式
|
||||
`yarn run dev` # 重新进入开发模式
|
||||
|
||||
注:Windows 开发模式运行之后会在底部任务栏的右下角应用区出现 PicList 的应用图标。
|
||||
|
||||
### 生产模式
|
||||
|
||||
如果你需要自行构建,可以 `yarn run build` 开始进行构建。构建成功后,会在 `dist_electron` 目录里出现构建成功的相应安装文件。
|
||||
|
||||
注意:如果你的网络环境不太好,可能会出现 `electron-builder` 下载 electron 二进制文件失败的情况。这个时候需要在 `npm run electron:build` 之前指定一下 electron 的源为国内源:
|
||||
|
||||
`export ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"`
|
||||
|
||||
**在 Windows 上,则可以使用 `set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/` (无需引号)**
|
||||
|
||||
## 其它相关
|
||||
|
||||
- [PicList-Core](https://github.com/Kuingsmile/PicList-Core) : 基于 PicGo-Core 二次开发的核心库,用于 CLI 操作和项目开发
|
||||
- [PicHoro](https://github.com/Kuingsmile/PicHoro): 与 PicList 搭配使用的手机端 APP
|
||||
- [VS-PicList](https://github.com/Kuingsmile/vs-PicList/): 与 PicList 搭配使用的VScode插件
|
||||
|
||||
## 交流群
|
||||
|
||||
如果有任何问题,可以加入TG群进行交流.
|
||||
|
||||
[PicList交流群](https://t.me/+rq8y7wsj7Pg5ZTg1)
|
||||
|
||||

|
||||
|
||||
## License
|
||||
|
||||
本项目基于MIT协议开源,欢迎大家使用和贡献代码,感谢原作者Molunerfinn的开源精神。
|
||||
|
||||
[MIT](https://opensource.org/licenses/MIT)
|
||||
|
||||
Copyright (c) 2017-present Molunerfinn
|
||||
|
||||
Copyright (c) 2023-present Kuingsmile
|
||||
|
||||
## Star Me
|
||||
|
||||
- Star 趋势 [](https://github.com/kuingsmile/PicList/stargazers)
|
||||
|
||||
[](https://github.com/kuingsmile/PicList/stargazers)
|
||||
<div align="center">
|
||||
<img src="https://imgx.horosama.com/admin_uploads/2022/10/2022_10_05_633d79e401694.png" alt="">
|
||||
<h1>PicList</h1>
|
||||
<a href="https://github.com/Kuingsmile/PicList/actions">
|
||||
<img src="https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/releases">
|
||||
<img src="https://img.shields.io/github/downloads/Kuingsmile/PicList/total.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/actions">
|
||||
<img src="https://github.com/Kuingsmile/PicList/actions/workflows/manually.yml/badge.svg" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/Kuingsmile/PicList/releases/latest">
|
||||
<img src="https://img.shields.io/github/release/Kuingsmile/PicList.svg?style=flat-square" alt="">
|
||||
</a>
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
简体中文 | [English](https://github.com/Kuingsmile/PicList/blob/dev/README.md)
|
||||
|
||||
PicList是一款高效的云存储和图床平台管理工具,在PicGo的基础上经过深度的二次开发,不仅完整保留了PicGo的所有功能,还增添了许多新的feature。例如相册支持同步云端删除文件,内置图床额外添加了WebDav、本地图床和SFTP等。PicList同时增加了完整的云存储管理功能,包括云端目录查看、文件搜索、批量上传下载和删除文件,复制多种格式文件链接和图片/markdown/文本/视频预览等,另外还有更加强大的相册和多项功能新增或优化。
|
||||
|
||||
## 如何从PicGo迁移
|
||||
|
||||
PicList `V1.5.0`以上版本提供 `一键迁移`功能,进入 `设置`页面,然后在 `从PicGo迁移`选项点击右侧按钮即可,迁移后请重启应用生效。
|
||||
|
||||
## PicList-Core
|
||||
|
||||
PicList的内核使用的是原版PicGo-Core基础上修改的[PicList-core](https://github.com/Kuingsmile/PicList-Core),为云端删除等功能做了适配,同时,新增了水印添加和图片压缩/缩放/旋转/格式转换等功能,可以通过CLI命令行调用,支持通过`picgo-server`命令开启上传服务器,还有一些其他的功能改动。
|
||||
|
||||
如果您希望使用PicList-core,请前往[https://github.com/Kuingsmile/PicList-Core](https://github.com/Kuingsmile/PicList-Core),或者前往[npm官方地址](https://www.npmjs.com/package/piclist)查看安装说明。
|
||||
|
||||
## 特色功能
|
||||
|
||||
- 保留了PicGo的所有功能,兼容绝大部分已有的PicGo插件,包括和Typora、Obsidian等软件的搭配
|
||||
- 新增了多个内置图床,如WebDav、兰空图床、本地图床、SFTP和Telegra.ph等,原内置imgur图床额外支持登录账号上传
|
||||
- 相册中可同步删除云端图片,支持所有内置图床和多个插件
|
||||
- 相册新增了高级搜索和排序,批量修改URL等功能
|
||||
- 内置水印添加、图片压缩、图片缩放、图片旋转和图片格式转换等功能,同时支持高级重命名
|
||||
- 上传接口支持表单上传文件,可多电脑共用
|
||||
- 支持配置同步至Github/Gitee/Gitea仓库
|
||||
- 支持管理十余种图床,可以在线进行云端目录查看、文件搜索、批量上传、批量下载、删除文件等
|
||||
- 支持预览多种格式的文件,包括图片、视频、纯文本文件和markdown文件等,具体支持的格式请参考[支持的文件格式列表](https://github.com/Kuingsmile/PicList/blob/dev/supported_format.md)
|
||||
- 支持启用正则表达式的批量云端文件重命名
|
||||
- 对于私有存储桶等支持复制预签名链接进行分享
|
||||
- 支持软件自动更新,支持多种启动模式,还有更多功能细节新增和优化
|
||||
- 优化了PicGo的界面,解锁了窗口大小限制,同时美化了部分界面布局
|
||||
- mac平台安装包已签名,从源头解决了PicGo上的安装包已损坏的日经问题
|
||||
|
||||
## 如何使用
|
||||
|
||||
### 如何在Vscode中使用
|
||||
|
||||
请安装我的配套插件 [VS-PicList](https://marketplace.visualstudio.com/items?itemName=Kuingsmile.vs-piclist),相比于vs-picgo插件,该插件直接依赖于PicList桌面端软件,支持多样上传和直接在Vscode中进行云端删除等功能。
|
||||
|
||||
### 如何在Typora中使用
|
||||
|
||||
#### **1.6.0版本及以上**
|
||||
|
||||
**Typora 1.6.0-dev以及以上版本现在已经原生支持PicList了**
|
||||
|
||||
目前Typora中需要同时设置上传服务PicList和PicGo(app)的路径为PicList的安装路径,已经在最新的Dev版本中修复。
|
||||
|
||||
[下载地址](https://typora.io/releases/all)
|
||||
|
||||
#### 1.6.0版本以下
|
||||
|
||||
Windows:
|
||||
|
||||
进入Typora设置界面,选择图像,将上传服务设置为 `PicGo(app)`,然后在 `PicGo路径`中填写PicList的安装路径,如下图所示:
|
||||
|
||||

|
||||
|
||||
或者,您也可以使用 `npm install piclist`命令安装PicList-core,然后上传服务设置为 `PicGo-Core(command line)`。
|
||||
|
||||
MacOS:
|
||||
|
||||
进入Typora设置界面,选择图像,将上传服务设置为 `Custom Command`,然后在 `Command`中填写 `/Applications/PicList.app/Contents/MacOS/PicList upload`,如下图所示:
|
||||
|
||||

|
||||
|
||||
验证上传选项可能会出现问题,可以忽略,直接使用是正常的。
|
||||
|
||||
### 如何在Obsidian中使用
|
||||
|
||||
在社区插件中搜索安装 `Image auto upload Plugin`,然后进入插件设置页面,修改默认上传器为 `PicGo(app)`,设置 `PicGo server`为 `http://127.0.0.1:36677/upload`即可,如下图所示, 此外该插件还额外支持通过PicList进行云端删除,请在删除接口内填入 `http://127.0.0.1:36677/delete`:
|
||||
|
||||

|
||||
|
||||
### 如何在Docker中使用
|
||||
|
||||
#### docker run
|
||||
|
||||
修改`./piclist`为你自己的配置文件`config.json`所在的路径,修改`piclist123456`为你自己的密钥。
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name piclist \
|
||||
--restart always \
|
||||
-p 36677:36677 \
|
||||
-v "./piclist:/root/.piclist" \
|
||||
kuingsmile/piclist:latest \
|
||||
node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
#### docker-compose
|
||||
|
||||
从piclist-core仓库下载`docker-compose.yml`文件,或者复制以下内容到`docker-compose.yml`文件中:
|
||||
|
||||
```yaml
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
node:
|
||||
image: 'kuingsmile/piclist:latest'
|
||||
container_name: piclist
|
||||
restart: always
|
||||
ports:
|
||||
- 36677:36677
|
||||
volumes:
|
||||
- './piclist:/root/.piclist'
|
||||
command: node /usr/local/bin/picgo-server -k piclist123456
|
||||
```
|
||||
|
||||
你可以修改`./piclist`为你自己的配置文件`config.json`所在的路径,修改`command`为你自己的密钥。
|
||||
|
||||
然后运行
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 已支持平台
|
||||
|
||||
| 平台 | 相册云删除 | 云存储管理 |
|
||||
| :------------: | :--------: | :--------: |
|
||||
| 内置AList | ✔️ | ✔️ |
|
||||
| SM.MS | ✔️ | ✔️ |
|
||||
| Github | ✔️ | ✔️ |
|
||||
| Imgur | ✔️ | ✔️ |
|
||||
| 腾讯COS V5 | ✔️ | ✔️ |
|
||||
| 阿里云OSS | ✔️ | ✔️ |
|
||||
| 又拍云 | ✔️ | ✔️ |
|
||||
| 七牛云 | ✔️ | ✔️ |
|
||||
| S3 API兼容平台 | ✔️ | ✔️ |
|
||||
| WebDAV | ✔️ | ✔️ |
|
||||
| 本地文件夹 | ✔️ | ✔️ |
|
||||
| 内置SFTP | ✔️ | ✔️ |
|
||||
| 多吉云 | ✔️ | ✔️ |
|
||||
| PicList(套娃) | ✔️ | ✔️ |
|
||||
| Telegra.ph | ✔️ | x |
|
||||
| 兰空图床 | ✔️ | ✔️ |
|
||||
|
||||
| 插件 | 相册云删除 |
|
||||
| :----------------------------------------------------------------------------------------: | :--------: |
|
||||
| [picgo-plugin-s3](https://github.com/wayjam/picgo-plugin-s3) | ✔️ |
|
||||
| [picgo-plugin-alist](https://github.com/jinzhi0123/picgo-plugin-alist) | ✔️ |
|
||||
| [picgo-plugin-huawei-uploader](https://github.com/YunfengGao/picgo-plugin-huawei-uploader) | ✔️ |
|
||||
| [picgo-plugin-dogecloud](https://github.com/w4j1e/picgo-plugin-dogecloud) | ✔️ |
|
||||
|
||||
## 下载安装
|
||||
|
||||
[https://github.com/Kuingsmile/PicList/releases/latest](https://github.com/Kuingsmile/PicList/releases/latest)
|
||||
|
||||
### Scoop
|
||||
|
||||
感谢[scoop-lemon](https://github.com/hoilc/scoop-lemon),你可以使用Scoop来安装PicList,只需要执行以下命令即可:
|
||||
|
||||
```bash
|
||||
scoop bucket add lemon https://github.com/hoilc/scoop-lemon
|
||||
scoop install lemon/piclist
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
|
||||
MacOS用户现在可以使用Homebrew来安装PicList了,只需要执行以下命令即可:
|
||||
|
||||
```bash
|
||||
brew install piclist --cask
|
||||
```
|
||||
|
||||
卸载命令:
|
||||
|
||||
```bash
|
||||
brew uninstall piclist
|
||||
```
|
||||
|
||||
### Mac特殊说明
|
||||
|
||||
如果macOS系统安装PicList后显示“文件已损坏”或者安装后打开无响应,请升级到PicList V1.4.1或以上版本。
|
||||
|
||||
V1.4.1之后,所有mac安装包都已经使用我的开发者证书签名,不会被macOS系统识别为“恶意软件”,不会出现“文件已损坏”提示。
|
||||
|
||||
## 应用截图
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 开发说明
|
||||
|
||||
1. 你需要有 Node、Git 环境,了解 npm 的相关知识。
|
||||
2. git clone [https://github.com/Kuingsmile/PicList.git](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)。
|
||||
|
||||
### 开发模式
|
||||
|
||||
输入 `yarn run dev` 进入开发模式,开发模式具有热重载特性。不过需要注意的是,开发模式不稳定,会有进程崩溃的情况。此时需要:
|
||||
|
||||
`ctrl+c` # 退出开发模式
|
||||
`yarn run dev` # 重新进入开发模式
|
||||
|
||||
注:Windows 开发模式运行之后会在底部任务栏的右下角应用区出现 PicList 的应用图标。
|
||||
|
||||
### 生产模式
|
||||
|
||||
如果你需要自行构建,可以 `yarn run build` 开始进行构建。构建成功后,会在 `dist_electron` 目录里出现构建成功的相应安装文件。
|
||||
|
||||
注意:如果你的网络环境不太好,可能会出现 `electron-builder` 下载 electron 二进制文件失败的情况。这个时候需要在 `npm run electron:build` 之前指定一下 electron 的源为国内源:
|
||||
|
||||
`export ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"`
|
||||
|
||||
**在 Windows 上,则可以使用 `set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/` (无需引号)**
|
||||
|
||||
## 其它相关
|
||||
|
||||
- [PicList-Core](https://github.com/Kuingsmile/PicList-Core) : 基于 PicGo-Core 二次开发的核心库,用于 CLI 操作和项目开发
|
||||
- [PicHoro](https://github.com/Kuingsmile/PicHoro): 与 PicList 搭配使用的手机端 APP
|
||||
- [VS-PicList](https://github.com/Kuingsmile/vs-PicList/): 与 PicList 搭配使用的VScode插件
|
||||
|
||||
## 交流群
|
||||
|
||||
如果有任何问题,可以加入TG群进行交流.
|
||||
|
||||
[PicList交流群](https://t.me/+rq8y7wsj7Pg5ZTg1)
|
||||
|
||||

|
||||
|
||||
## License
|
||||
|
||||
本项目基于MIT协议开源,欢迎大家使用和贡献代码,感谢原作者Molunerfinn的开源精神。
|
||||
|
||||
[MIT](https://opensource.org/licenses/MIT)
|
||||
|
||||
Copyright (c) 2017-present Molunerfinn
|
||||
|
||||
Copyright (c) 2023-present Kuingsmile
|
||||
|
||||
## Star Me
|
||||
|
||||
- Star 趋势 [](https://github.com/kuingsmile/PicList/stargazers)
|
||||
|
||||
[](https://github.com/kuingsmile/PicList/stargazers)
|
||||
|
||||
+3
-5
@@ -1,6 +1,4 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
],
|
||||
plugins: ['@babel/plugin-proposal-optional-chaining']
|
||||
}
|
||||
presets: ["@vue/cli-plugin-babel/preset"],
|
||||
plugins: ["@babel/plugin-proposal-optional-chaining"],
|
||||
};
|
||||
|
||||
+14
-32
@@ -1,32 +1,14 @@
|
||||
### ✨ Features
|
||||
|
||||
- 图床
|
||||
- 现在`sm.ms`图床上传重复图片时,后上传的图片也支持云端删除了
|
||||
- 管理
|
||||
- 现在s3图床支持新建存储通
|
||||
- 调整了管理页面的部分布局
|
||||
- 代码高亮样式调整为stackoverflow-light
|
||||
- 界面
|
||||
- 现在托盘菜单会根据当前状态显示开启/关闭剪贴板监听,而不是始终显示全部菜单
|
||||
- 现在托盘菜单会根据当前状态显示开启/关闭mini窗口
|
||||
- 现在鼠标悬停于托盘图标时,会显示当前图床和配置名
|
||||
- 移除了设置页面部分不必要的全局通知
|
||||
- 优化了英文下的设置页面布局
|
||||
- 性能
|
||||
- 现在启动软件时会自动清空剪贴板图片缓存文件夹
|
||||
- 优化了读取配置的性能表现
|
||||
- 优化了多个页面的加载速度
|
||||
- 升级vue等依赖至最新版本
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- 修复了从obsidan发送删除请求时,会导致软件闪退的问题
|
||||
- 修复了使用`webp`插件时,上传剪贴板图片会导致软件闪退的问题
|
||||
- 修复了设置了图片处理操作后,上传URL会报错的问题
|
||||
- 修复了设置原始PicGo窗口大小后,重新打开设置界面时窗口大小数值没有更新的问题
|
||||
- 修复了主界面菜单加载和点击时反复触发获取图床配置导致的卡顿问题
|
||||
- 修复了切换语言时部分下拉框没有更新的问题
|
||||
- 修复了部分翻译错误
|
||||
- 管理页面
|
||||
- 修复了部分图床的文件夹复制链接错误的问题
|
||||
- 修复了文件浏览页面的css错误
|
||||
### ✨ Features
|
||||
|
||||
- 新增内置AList上传支持
|
||||
- 优化了设置了图片处理操作时的上传进度通知
|
||||
- 优化了部分图床的i18n设置
|
||||
- 修改内置兰空图床显示名,由`lsky`修改为`兰空图床`
|
||||
- core版本现在使用piclist桌面版配置文件时支持设置`configName`参数
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- 修复了s3图床文件名含有特殊字符时无法删除的问题
|
||||
- 修复了特定情况下开启手动重命名后会导致软件闪退的问题
|
||||
- 修复了图床设置页面按钮的样式问题
|
||||
- 优化了多个图床存在的重复记录错误日志的问题
|
||||
+14
-32
@@ -1,32 +1,14 @@
|
||||
### ✨ Features
|
||||
|
||||
- PicBed
|
||||
- Now when uploading duplicate images to `sm.ms` image bed, the later uploaded image also supports cloud deletion
|
||||
- Management
|
||||
- Now s3 image bed supports creating new storage channels
|
||||
- Adjusted some layouts of the management page
|
||||
- The code highlighting style is adjusted to stackoverflow-light
|
||||
- Interface
|
||||
- Now the tray menu will display the open/close clipboard monitoring according to the current status, instead of always displaying all menus
|
||||
- Now the tray menu will display the open/close mini window according to the current status
|
||||
- Now when the mouse hovers over the tray icon, the current image bed and configuration name will be displayed
|
||||
- Removed some unnecessary global notifications on the settings page
|
||||
- Optimized the layout of the settings page in English
|
||||
- Performance
|
||||
- Now the software will automatically clear the clipboard image cache folder when starting
|
||||
- Optimized the performance of reading configuration
|
||||
- Optimized the loading speed of multiple pages
|
||||
- Upgrade dependencies such as vue to the latest version
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Fix the problem that the software crashes when sending a delete request from obsidan
|
||||
- Fix the problem that the software crashes when uploading clipboard images using the `webp` plugin
|
||||
- Fix the problem that an error occurs when uploading a URL after setting image processing operations
|
||||
- Fix the problem that the window size value is not updated when the original PicGo window size is set and the setting interface is reopened
|
||||
- Fix the problem that the main interface menu repeatedly triggers the lag caused by repeatedly triggering the acquisition of the image bed configuration when loading and clicking
|
||||
- Fix the problem that some drop-down boxes are not updated when switching languages
|
||||
- Fix some translation errors
|
||||
- Management page
|
||||
- Fix the problem that the folder copy link of some image beds is incorrect
|
||||
- Fix the css error of the file browsing page
|
||||
### ✨ Features
|
||||
|
||||
- Add built-in AList upload support
|
||||
- Optimize the upload progress notification when image processing operations are set
|
||||
- Optimize the i18n settings of some image beds
|
||||
- Modify the display name of the built-in Lan Kong image bed from `lsky` to `Lan Kong image bed`
|
||||
- The core version now supports setting the `configName` parameter when using the piclist desktop version configuration file
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Fix the problem that the s3 image bed cannot be deleted when the file name contains special characters
|
||||
- Fix the problem that the software crashes under specific conditions after enabling manual renaming
|
||||
- Fix the style problem of the button on the image bed setting page
|
||||
- Optimize the problem of repeated recording error logs for multiple image beds
|
||||
|
||||
+16
-20
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "piclist",
|
||||
"version": "2.9.0",
|
||||
"version": "2.9.1",
|
||||
"author": {
|
||||
"name": "Kuingsmile",
|
||||
"email": "pkukuing@gmail.com"
|
||||
@@ -15,23 +15,23 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vue-cli-service electron:build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"bump": "bump-version",
|
||||
"cz": "git-cz",
|
||||
"dev": "vue-cli-service electron:serve",
|
||||
"electron:build": "vue-cli-service electron:build",
|
||||
"electron:serve": "vue-cli-service electron:serve",
|
||||
"i18n": "node ./scripts/gen-i18n-types.js",
|
||||
"link": "node ./scripts/link.js",
|
||||
"lint": "vue-cli-service lint",
|
||||
"lint:dpdm": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/background.ts",
|
||||
"lint:fix": "eslint --fix --ext .js,.jsx,.ts,.tsx,.vue src/",
|
||||
"ncu": "node ./scripts/check-dep.js",
|
||||
"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-r2.js",
|
||||
"upload-beta": "node ./scripts/upload-beta.js",
|
||||
"link": "node ./scripts/link.js",
|
||||
"sha256": "node ./scripts/gen-sha256.js",
|
||||
"ncu": "node ./scripts/check-dep.js",
|
||||
"lint:dpdm": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/background.ts"
|
||||
"upload-beta": "node ./scripts/upload-beta.js",
|
||||
"upload-dist": "node ./scripts/upload-dist-to-r2.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.421.0",
|
||||
@@ -52,7 +52,7 @@
|
||||
"cos-nodejs-sdk-v5": "^2.12.5",
|
||||
"dexie": "^3.2.4",
|
||||
"electron-updater": "^6.1.4",
|
||||
"element-plus": "2.7.4",
|
||||
"element-plus": "2.7.6",
|
||||
"epipebomb": "^1.0.0",
|
||||
"fast-xml-parser": "^4.3.2",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -67,7 +67,7 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-ssh-no-cpu-features": "^2.0.0",
|
||||
"nodejs-file-downloader": "^4.12.1",
|
||||
"piclist": "^1.8.10",
|
||||
"piclist": "^1.9.0",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"proxy-agent": "^5.0.0",
|
||||
@@ -79,15 +79,14 @@
|
||||
"upyun": "^3.4.6",
|
||||
"uuid": "^9.0.1",
|
||||
"video.js": "^8.6.1",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "^4.3.2",
|
||||
"vue": "^3.4.30",
|
||||
"vue-router": "^4.4.0",
|
||||
"vue3-lazyload": "^0.3.8",
|
||||
"vue3-photo-preview": "^0.3.0",
|
||||
"webdav": "^5.3.1",
|
||||
"write-file-atomic": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/video.js": "^7.3.58",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||
"@electron/notarize": "^2.1.0",
|
||||
"@types/ali-oss": "^6.16.11",
|
||||
@@ -103,32 +102,29 @@
|
||||
"@types/tunnel": "^0.0.7",
|
||||
"@types/upyun": "^3.4.3",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/video.js": "^7.3.58",
|
||||
"@types/write-file-atomic": "^4.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"@vue/cli-plugin-babel": "^5.0.8",
|
||||
"@vue/cli-plugin-eslint": "^5.0.8",
|
||||
"@vue/cli-plugin-router": "^5.0.8",
|
||||
"@vue/cli-plugin-typescript": "^5.0.8",
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-standard": "^8.0.1",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/runtime-dom": "^3.4.27",
|
||||
"@vue/runtime-dom": "^3.4.30",
|
||||
"conventional-changelog": "^5.1.0",
|
||||
"cz-customizable": "^7.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"dpdm": "^3.14.0",
|
||||
"electron": "^22.3.27",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-promise": "^6.2.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"husky": "^3.1.0",
|
||||
"node-bump-version": "^1.0.2",
|
||||
"node-loader": "^2.0.0",
|
||||
"npm-check-updates": "^16.14.20",
|
||||
"prettier": "^3.3.2",
|
||||
"stylus": "^0.59.0",
|
||||
"stylus-loader": "^7.1.3",
|
||||
"typescript": "^4.9.5",
|
||||
|
||||
+997
-997
File diff suppressed because it is too large
Load Diff
+1004
-1004
File diff suppressed because it is too large
Load Diff
+997
-997
File diff suppressed because it is too large
Load Diff
@@ -51,4 +51,5 @@ app.use(pinia)
|
||||
app.use(hljsVuePlugin)
|
||||
app.use(VueVideoPlayer)
|
||||
app.mount('#app')
|
||||
|
||||
initTalkingData()
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import axios from 'axios'
|
||||
import {
|
||||
app,
|
||||
clipboard,
|
||||
dialog,
|
||||
shell
|
||||
} from 'electron'
|
||||
import { app, clipboard, dialog, shell } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { gte, lte } from 'semver'
|
||||
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
import { showNotification } from '~/utils/common'
|
||||
import {
|
||||
IRemoteNoticeActionType,
|
||||
IRemoteNoticeTriggerCount,
|
||||
IRemoteNoticeTriggerHook
|
||||
} from '#/types/enum'
|
||||
import { IRemoteNoticeActionType, IRemoteNoticeTriggerCount, IRemoteNoticeTriggerHook } from '#/types/enum'
|
||||
|
||||
// for test
|
||||
const REMOTE_NOTICE_URL = 'https://release.piclist.cn/remote-notice.json'
|
||||
@@ -30,38 +21,41 @@ class RemoteNoticeHandler {
|
||||
private remoteNotice: IRemoteNotice | null = null
|
||||
private remoteNoticeLocalCountStorage: IRemoteNoticeLocalCountStorage | null = null
|
||||
|
||||
async init () {
|
||||
async init() {
|
||||
this.remoteNotice = await this.getRemoteNoticeInfo()
|
||||
this.initLocalCountStorage()
|
||||
}
|
||||
|
||||
private initLocalCountStorage () {
|
||||
private initLocalCountStorage() {
|
||||
const localCountStorage = {}
|
||||
if (!fs.existsSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH)) {
|
||||
fs.writeFileSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, JSON.stringify({}))
|
||||
}
|
||||
try {
|
||||
const localCountStorage: IRemoteNoticeLocalCountStorage = fs.readJSONSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, 'utf8')
|
||||
const localCountStorage: IRemoteNoticeLocalCountStorage = fs.readJSONSync(
|
||||
REMOTE_NOTICE_LOCAL_STORAGE_PATH,
|
||||
'utf8'
|
||||
)
|
||||
this.remoteNoticeLocalCountStorage = localCountStorage
|
||||
} catch (e) {
|
||||
this.remoteNoticeLocalCountStorage = localCountStorage
|
||||
}
|
||||
}
|
||||
|
||||
private saveLocalCountStorage (newData?: IRemoteNoticeLocalCountStorage) {
|
||||
private saveLocalCountStorage(newData?: IRemoteNoticeLocalCountStorage) {
|
||||
if (newData) {
|
||||
this.remoteNoticeLocalCountStorage = newData
|
||||
}
|
||||
fs.writeFileSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, JSON.stringify(this.remoteNoticeLocalCountStorage))
|
||||
}
|
||||
|
||||
private async getRemoteNoticeInfo (): Promise<IRemoteNotice | null> {
|
||||
private async getRemoteNoticeInfo(): Promise<IRemoteNotice | null> {
|
||||
try {
|
||||
const noticeInfo = await axios({
|
||||
const noticeInfo = (await axios({
|
||||
method: 'get',
|
||||
url: REMOTE_NOTICE_URL,
|
||||
responseType: 'json'
|
||||
}).then(res => res.data) as IRemoteNotice
|
||||
}).then(res => res.data)) as IRemoteNotice
|
||||
return noticeInfo
|
||||
} catch {
|
||||
return null
|
||||
@@ -72,7 +66,7 @@ class RemoteNoticeHandler {
|
||||
* if the notice is not shown or is always shown, then show the notice
|
||||
* @param action
|
||||
*/
|
||||
private checkActionCount (action: IRemoteNoticeAction) {
|
||||
private checkActionCount(action: IRemoteNoticeAction) {
|
||||
try {
|
||||
if (!this.remoteNoticeLocalCountStorage) {
|
||||
return true
|
||||
@@ -106,7 +100,7 @@ class RemoteNoticeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async doActions (actions: IRemoteNoticeAction[]) {
|
||||
private async doActions(actions: IRemoteNoticeAction[]) {
|
||||
for (const action of actions) {
|
||||
if (this.checkActionCount(action)) {
|
||||
switch (action.type) {
|
||||
@@ -121,7 +115,7 @@ class RemoteNoticeHandler {
|
||||
body: action.data?.content || '',
|
||||
clickToCopy: !!action.data?.copyToClipboard,
|
||||
copyContent: action.data?.copyToClipboard || '',
|
||||
clickFn () {
|
||||
clickFn() {
|
||||
if (action.data?.url) {
|
||||
shell.openExternal(action.data.url)
|
||||
}
|
||||
@@ -129,11 +123,11 @@ class RemoteNoticeHandler {
|
||||
})
|
||||
break
|
||||
case IRemoteNoticeActionType.OPEN_URL:
|
||||
// OPEN URL
|
||||
// OPEN URL
|
||||
shell.openExternal(action.data?.url || '')
|
||||
break
|
||||
case IRemoteNoticeActionType.COMMON:
|
||||
// DO COMMON CASE
|
||||
// DO COMMON CASE
|
||||
if (action.data?.copyToClipboard) {
|
||||
clipboard.writeText(action.data.copyToClipboard)
|
||||
}
|
||||
@@ -143,21 +137,23 @@ class RemoteNoticeHandler {
|
||||
break
|
||||
case IRemoteNoticeActionType.SHOW_MESSAGE_BOX: {
|
||||
const currentWindow = windowManager.getAvailableWindow()
|
||||
dialog.showMessageBox(currentWindow, {
|
||||
title: action.data?.title || '',
|
||||
message: action.data?.content || '',
|
||||
type: 'info',
|
||||
buttons: action.data?.buttons?.map(item => item.label) || ['Yes']
|
||||
}).then(res => {
|
||||
const button = action.data?.buttons?.[res.response]
|
||||
if (button?.type === 'cancel') {
|
||||
// do nothing
|
||||
} else {
|
||||
if (button?.action) {
|
||||
this.doActions([button?.action])
|
||||
dialog
|
||||
.showMessageBox(currentWindow, {
|
||||
title: action.data?.title || '',
|
||||
message: action.data?.content || '',
|
||||
type: 'info',
|
||||
buttons: action.data?.buttons?.map(item => item.label) || ['Yes']
|
||||
})
|
||||
.then(res => {
|
||||
const button = action.data?.buttons?.[res.response]
|
||||
if (button?.type === 'cancel') {
|
||||
// do nothing
|
||||
} else {
|
||||
if (button?.action) {
|
||||
this.doActions([button?.action])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -165,7 +161,7 @@ class RemoteNoticeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
triggerHook (hook: IRemoteNoticeTriggerHook) {
|
||||
triggerHook(hook: IRemoteNoticeTriggerHook) {
|
||||
if (!this.remoteNotice || !this.remoteNotice.list) {
|
||||
return
|
||||
}
|
||||
@@ -198,6 +194,4 @@ class RemoteNoticeHandler {
|
||||
|
||||
const remoteNoticeHandler = new RemoteNoticeHandler()
|
||||
|
||||
export {
|
||||
remoteNoticeHandler
|
||||
}
|
||||
export { remoteNoticeHandler }
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
|
||||
import {
|
||||
globalShortcut
|
||||
} from 'electron'
|
||||
import { globalShortcut } from 'electron'
|
||||
|
||||
import shortKeyService from 'apis/app/shortKey/shortKeyService'
|
||||
import GuiApi from 'apis/gui'
|
||||
@@ -16,18 +13,18 @@ import { configPaths } from '#/utils/configPaths'
|
||||
|
||||
class ShortKeyHandler {
|
||||
private isInModifiedMode: boolean = false
|
||||
constructor () {
|
||||
constructor() {
|
||||
bus.on(TOGGLE_SHORTKEY_MODIFIED_MODE, flag => {
|
||||
this.isInModifiedMode = flag
|
||||
})
|
||||
}
|
||||
|
||||
init () {
|
||||
init() {
|
||||
this.initBuiltInShortKey()
|
||||
this.initPluginsShortKey()
|
||||
}
|
||||
|
||||
private initBuiltInShortKey () {
|
||||
private initBuiltInShortKey() {
|
||||
const commands = db.get(configPaths.settings.shortKey._path) as IShortKeyConfigs
|
||||
Object.keys(commands)
|
||||
.filter(item => item.includes('picgo:'))
|
||||
@@ -42,7 +39,7 @@ class ShortKeyHandler {
|
||||
})
|
||||
}
|
||||
|
||||
private initPluginsShortKey () {
|
||||
private initPluginsShortKey() {
|
||||
// get enabled plugin
|
||||
const pluginList = picgo.pluginLoader.getList()
|
||||
for (const item of pluginList) {
|
||||
@@ -72,7 +69,12 @@ class ShortKeyHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private registerShortKey (config: IShortKeyConfig | IPluginShortKeyConfig, command: string, handler: IShortKeyHandler, writeFlag: boolean) {
|
||||
private registerShortKey(
|
||||
config: IShortKeyConfig | IPluginShortKeyConfig,
|
||||
command: string,
|
||||
handler: IShortKeyHandler,
|
||||
writeFlag: boolean
|
||||
) {
|
||||
shortKeyService.registerCommand(command, handler)
|
||||
if (config.key) {
|
||||
globalShortcut.register(config.key, () => {
|
||||
@@ -96,7 +98,7 @@ class ShortKeyHandler {
|
||||
}
|
||||
|
||||
// enable or disable shortKey
|
||||
bindOrUnbindShortKey (item: IShortKeyConfig, from: string): boolean {
|
||||
bindOrUnbindShortKey(item: IShortKeyConfig, from: string): boolean {
|
||||
const command = `${from}:${item.name}`
|
||||
if (item.enable === false) {
|
||||
globalShortcut.unregister(item.key)
|
||||
@@ -120,7 +122,7 @@ class ShortKeyHandler {
|
||||
}
|
||||
|
||||
// update shortKey bindings
|
||||
updateShortKey (item: IShortKeyConfig, oldKey: string, from: string): boolean {
|
||||
updateShortKey(item: IShortKeyConfig, oldKey: string, from: string): boolean {
|
||||
const command = `${from}:${item.name}`
|
||||
if (globalShortcut.isRegistered(item.key)) return false
|
||||
globalShortcut.unregister(oldKey)
|
||||
@@ -133,7 +135,7 @@ class ShortKeyHandler {
|
||||
return true
|
||||
}
|
||||
|
||||
private async handler (command: string) {
|
||||
private async handler(command: string) {
|
||||
if (this.isInModifiedMode) {
|
||||
return
|
||||
}
|
||||
@@ -149,7 +151,7 @@ class ShortKeyHandler {
|
||||
}
|
||||
}
|
||||
|
||||
registerPluginShortKey (pluginName: string) {
|
||||
registerPluginShortKey(pluginName: string) {
|
||||
const plugin = picgo.pluginLoader.getPlugin(pluginName)
|
||||
if (plugin && plugin.commands) {
|
||||
if (typeof plugin.commands !== 'function') {
|
||||
@@ -169,7 +171,7 @@ class ShortKeyHandler {
|
||||
}
|
||||
}
|
||||
|
||||
unregisterPluginShortKey (pluginName: string) {
|
||||
unregisterPluginShortKey(pluginName: string) {
|
||||
const commands = db.get(configPaths.settings.shortKey._path) as IShortKeyConfigs
|
||||
const keyList = Object.keys(commands)
|
||||
.filter(command => command.includes(pluginName))
|
||||
|
||||
@@ -2,22 +2,22 @@ import logger from '@core/picgo/logger'
|
||||
|
||||
class ShortKeyService {
|
||||
private commandList: Map<string, IShortKeyHandler> = new Map()
|
||||
registerCommand (command: string, handler: IShortKeyHandler) {
|
||||
registerCommand(command: string, handler: IShortKeyHandler) {
|
||||
this.commandList.set(command, handler)
|
||||
}
|
||||
|
||||
unregisterCommand (command: string) {
|
||||
unregisterCommand(command: string) {
|
||||
this.commandList.delete(command)
|
||||
}
|
||||
|
||||
getShortKeyHandler (command: string): IShortKeyHandler | null {
|
||||
getShortKeyHandler(command: string): IShortKeyHandler | null {
|
||||
const handler = this.commandList.get(command)
|
||||
if (handler) return handler
|
||||
logger.warn(`cannot find command: ${command}`)
|
||||
return null
|
||||
}
|
||||
|
||||
getCommandList () {
|
||||
getCommandList() {
|
||||
return [...this.commandList.keys()]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import { hideMiniWindow, openMainWindow, openMiniWindow } from '~/utils/windowHe
|
||||
|
||||
let contextMenu: Menu | null
|
||||
|
||||
export function setDockMenu () {
|
||||
export function setDockMenu() {
|
||||
const isListeningClipboard = db.get(configPaths.settings.isListeningClipboard) || false
|
||||
const dockMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
@@ -43,7 +43,7 @@ export function setDockMenu () {
|
||||
},
|
||||
{
|
||||
label: T('START_WATCH_CLIPBOARD'),
|
||||
click () {
|
||||
click() {
|
||||
db.set(configPaths.settings.isListeningClipboard, true)
|
||||
clipboardPoll.startListening()
|
||||
clipboardPoll.on('change', () => {
|
||||
@@ -56,7 +56,7 @@ export function setDockMenu () {
|
||||
},
|
||||
{
|
||||
label: T('STOP_WATCH_CLIPBOARD'),
|
||||
click () {
|
||||
click() {
|
||||
db.set(configPaths.settings.isListeningClipboard, false)
|
||||
clipboardPoll.stopListening()
|
||||
clipboardPoll.removeAllListeners()
|
||||
@@ -68,14 +68,20 @@ export function setDockMenu () {
|
||||
app.dock.setMenu(dockMenu)
|
||||
}
|
||||
|
||||
export function createMenu () {
|
||||
export function createMenu() {
|
||||
const submenu = buildPicBedListMenu()
|
||||
const appMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'PicList',
|
||||
submenu: [
|
||||
{ label: T('OPEN_MAIN_WINDOW'), click: openMainWindow },
|
||||
{ label: T('RELOAD_APP'), click () { app.relaunch(); app.exit(0) } }
|
||||
{
|
||||
label: T('RELOAD_APP'),
|
||||
click() {
|
||||
app.relaunch()
|
||||
app.exit(0)
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ label: T('CHOOSE_DEFAULT_PICBED'), type: 'submenu', submenu },
|
||||
@@ -93,18 +99,17 @@ export function createMenu () {
|
||||
},
|
||||
{
|
||||
label: T('QUIT'),
|
||||
submenu: [
|
||||
{ label: T('QUIT'), role: 'quit' }
|
||||
]
|
||||
submenu: [{ label: T('QUIT'), role: 'quit' }]
|
||||
}
|
||||
])
|
||||
Menu.setApplicationMenu(appMenu)
|
||||
}
|
||||
|
||||
export function createContextMenu () {
|
||||
export function createContextMenu() {
|
||||
const ClipboardWatcher = clipboardPoll
|
||||
const isListeningClipboard = db.get(configPaths.settings.isListeningClipboard) || false
|
||||
const isMiniWindowVisible = windowManager.has(IWindowList.MINI_WINDOW) && windowManager.get(IWindowList.MINI_WINDOW)!.isVisible()
|
||||
const isMiniWindowVisible =
|
||||
windowManager.has(IWindowList.MINI_WINDOW) && windowManager.get(IWindowList.MINI_WINDOW)!.isVisible()
|
||||
|
||||
const startWatchClipboard = () => {
|
||||
db.set(configPaths.settings.isListeningClipboard, true)
|
||||
@@ -125,18 +130,44 @@ export function createContextMenu () {
|
||||
|
||||
if (process.platform === 'darwin' || process.platform === 'win32') {
|
||||
const submenu = buildPicBedListMenu()
|
||||
const template: Array<(MenuItemConstructorOptions) | (MenuItem)> = [
|
||||
const template: Array<MenuItemConstructorOptions | MenuItem> = [
|
||||
{ label: T('OPEN_MAIN_WINDOW'), click: openMainWindow },
|
||||
{ label: T('CHOOSE_DEFAULT_PICBED'), type: 'submenu', submenu },
|
||||
{ label: T('START_WATCH_CLIPBOARD'), click: startWatchClipboard, visible: !isListeningClipboard },
|
||||
{ label: T('STOP_WATCH_CLIPBOARD'), click: stopWatchClipboard, visible: isListeningClipboard },
|
||||
{ label: T('RELOAD_APP'), click () { app.relaunch(); app.exit(0) } },
|
||||
{
|
||||
label: T('START_WATCH_CLIPBOARD'),
|
||||
click: startWatchClipboard,
|
||||
visible: !isListeningClipboard
|
||||
},
|
||||
{
|
||||
label: T('STOP_WATCH_CLIPBOARD'),
|
||||
click: stopWatchClipboard,
|
||||
visible: isListeningClipboard
|
||||
},
|
||||
{
|
||||
label: T('RELOAD_APP'),
|
||||
click() {
|
||||
app.relaunch()
|
||||
app.exit(0)
|
||||
}
|
||||
},
|
||||
{ label: T('QUIT'), role: 'quit' }
|
||||
]
|
||||
if (process.platform === 'win32') {
|
||||
template.splice(2, 0,
|
||||
{ label: T('OPEN_MINI_WINDOW'), click () { openMiniWindow(false) }, visible: !isMiniWindowVisible },
|
||||
{ label: T('HIDE_MINI_WINDOW'), click: hideMiniWindow, visible: isMiniWindowVisible }
|
||||
template.splice(
|
||||
2,
|
||||
0,
|
||||
{
|
||||
label: T('OPEN_MINI_WINDOW'),
|
||||
click() {
|
||||
openMiniWindow(false)
|
||||
},
|
||||
visible: !isMiniWindowVisible
|
||||
},
|
||||
{
|
||||
label: T('HIDE_MINI_WINDOW'),
|
||||
click: hideMiniWindow,
|
||||
visible: isMiniWindowVisible
|
||||
}
|
||||
)
|
||||
}
|
||||
contextMenu = Menu.buildFromTemplate(template)
|
||||
@@ -150,13 +181,31 @@ export function createContextMenu () {
|
||||
|
||||
contextMenu = Menu.buildFromTemplate([
|
||||
{ label: T('OPEN_MAIN_WINDOW'), click: openMainWindow },
|
||||
{ label: T('OPEN_MINI_WINDOW'), click () { openMiniWindow(false) }, visible: !isMiniWindowVisible },
|
||||
{ label: T('HIDE_MINI_WINDOW'), click: hideMiniWindow, visible: isMiniWindowVisible },
|
||||
{ label: T('START_WATCH_CLIPBOARD'), click: startWatchClipboard, visible: !isListeningClipboard },
|
||||
{ label: T('STOP_WATCH_CLIPBOARD'), click: stopWatchClipboard, visible: isListeningClipboard },
|
||||
{
|
||||
label: T('OPEN_MINI_WINDOW'),
|
||||
click() {
|
||||
openMiniWindow(false)
|
||||
},
|
||||
visible: !isMiniWindowVisible
|
||||
},
|
||||
{
|
||||
label: T('HIDE_MINI_WINDOW'),
|
||||
click: hideMiniWindow,
|
||||
visible: isMiniWindowVisible
|
||||
},
|
||||
{
|
||||
label: T('START_WATCH_CLIPBOARD'),
|
||||
click: startWatchClipboard,
|
||||
visible: !isListeningClipboard
|
||||
},
|
||||
{
|
||||
label: T('STOP_WATCH_CLIPBOARD'),
|
||||
click: stopWatchClipboard,
|
||||
visible: isListeningClipboard
|
||||
},
|
||||
{
|
||||
label: T('ABOUT'),
|
||||
click () {
|
||||
click() {
|
||||
dialog.showMessageBox({
|
||||
title: 'PicList',
|
||||
message: 'PicList',
|
||||
@@ -179,7 +228,7 @@ const getTrayIcon = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export function createTray (tooltip: string) {
|
||||
export function createTray(tooltip: string) {
|
||||
const menubarPic = getTrayIcon()
|
||||
setTray(new Tray(menubarPic))
|
||||
tray.setToolTip(tooltip)
|
||||
@@ -260,9 +309,7 @@ export function createTray (tooltip: string) {
|
||||
const pasteStyle = db.get(configPaths.settings.pasteStyle) || IPasteStyle.MARKDOWN
|
||||
const rawInput = cloneDeep(files)
|
||||
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
|
||||
const imgs = await uploader
|
||||
.setWebContents(trayWindow.webContents)
|
||||
.upload(files)
|
||||
const imgs = await uploader.setWebContents(trayWindow.webContents).upload(files)
|
||||
const deleteLocalFile = db.get(configPaths.settings.deleteLocalFile) || false
|
||||
if (imgs !== false) {
|
||||
const pasteText: string[] = []
|
||||
@@ -270,8 +317,11 @@ export function createTray (tooltip: string) {
|
||||
if (deleteLocalFile) {
|
||||
await fs.remove(rawInput[i])
|
||||
}
|
||||
pasteText.push(await (pasteTemplate(pasteStyle, imgs[i], db.get(configPaths.settings.customLink))))
|
||||
const isShowResultNotification = db.get(configPaths.settings.uploadResultNotification) === undefined ? true : !!db.get(configPaths.settings.uploadResultNotification)
|
||||
pasteText.push(await pasteTemplate(pasteStyle, imgs[i], db.get(configPaths.settings.customLink)))
|
||||
const isShowResultNotification =
|
||||
db.get(configPaths.settings.uploadResultNotification) === undefined
|
||||
? true
|
||||
: !!db.get(configPaths.settings.uploadResultNotification)
|
||||
if (isShowResultNotification) {
|
||||
const notification = new Notification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
@@ -290,8 +340,8 @@ export function createTray (tooltip: string) {
|
||||
})
|
||||
// toggleWindow()
|
||||
} else if (process.platform === 'linux') {
|
||||
// click事件在Ubuntu上无法触发,Unity不支持(在Mac和Windows上可以触发)
|
||||
// 需要使用 setContextMenu 设置菜单
|
||||
// click事件在Ubuntu上无法触发,Unity不支持(在Mac和Windows上可以触发)
|
||||
// 需要使用 setContextMenu 设置菜单
|
||||
createContextMenu()
|
||||
tray!.setContextMenu(contextMenu)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
Notification,
|
||||
WebContents
|
||||
} from 'electron'
|
||||
import { Notification, WebContents } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
@@ -19,7 +16,10 @@ import { IPasteStyle, IWindowList } from '#/types/enum'
|
||||
import { configPaths } from '#/utils/configPaths'
|
||||
|
||||
const handleClipboardUploading = async (): Promise<false | ImgInfo[]> => {
|
||||
const useBuiltinClipboard = db.get(configPaths.settings.useBuiltinClipboard) === undefined ? true : !!db.get(configPaths.settings.useBuiltinClipboard)
|
||||
const useBuiltinClipboard =
|
||||
db.get(configPaths.settings.useBuiltinClipboard) === undefined
|
||||
? true
|
||||
: !!db.get(configPaths.settings.useBuiltinClipboard)
|
||||
const win = windowManager.getAvailableWindow()
|
||||
if (useBuiltinClipboard) {
|
||||
return await uploader.setWebContents(win!.webContents).uploadWithBuildInClipboard()
|
||||
@@ -33,8 +33,11 @@ export const uploadClipboardFiles = async (): Promise<IStringKeyMap> => {
|
||||
if (img.length > 0) {
|
||||
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)
|
||||
const pasteStyle = db.get(configPaths.settings.pasteStyle) || IPasteStyle.MARKDOWN
|
||||
handleCopyUrl(await (pasteTemplate(pasteStyle, img[0], db.get(configPaths.settings.customLink))))
|
||||
const isShowResultNotification = db.get(configPaths.settings.uploadResultNotification) === undefined ? true : !!db.get(configPaths.settings.uploadResultNotification)
|
||||
handleCopyUrl(await pasteTemplate(pasteStyle, img[0], db.get(configPaths.settings.customLink)))
|
||||
const isShowResultNotification =
|
||||
db.get(configPaths.settings.uploadResultNotification) === undefined
|
||||
? true
|
||||
: !!db.get(configPaths.settings.uploadResultNotification)
|
||||
if (isShowResultNotification) {
|
||||
const notification = new Notification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
@@ -75,7 +78,10 @@ export const uploadClipboardFiles = async (): Promise<IStringKeyMap> => {
|
||||
}
|
||||
}
|
||||
|
||||
export const uploadChoosedFiles = async (webContents: WebContents, files: IFileWithPath[]): Promise<IStringKeyMap[]> => {
|
||||
export const uploadChoosedFiles = async (
|
||||
webContents: WebContents,
|
||||
files: IFileWithPath[]
|
||||
): Promise<IStringKeyMap[]> => {
|
||||
const input = files.map(item => item.path)
|
||||
const rawInput = cloneDeep(input)
|
||||
const imgs = await uploader.setWebContents(webContents).upload(input)
|
||||
@@ -86,14 +92,19 @@ export const uploadChoosedFiles = async (webContents: WebContents, files: IFileW
|
||||
const pasteText: string[] = []
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
if (deleteLocalFile) {
|
||||
fs.remove(rawInput[i]).then(() => {
|
||||
picgo.log.info(`delete local file: ${rawInput[i]}`)
|
||||
}).catch((err: Error) => {
|
||||
picgo.log.error(err)
|
||||
})
|
||||
fs.remove(rawInput[i])
|
||||
.then(() => {
|
||||
picgo.log.info(`delete local file: ${rawInput[i]}`)
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
picgo.log.error(err)
|
||||
})
|
||||
}
|
||||
pasteText.push(await (pasteTemplate(pasteStyle, imgs[i], db.get(configPaths.settings.customLink))))
|
||||
const isShowResultNotification = db.get(configPaths.settings.uploadResultNotification) === undefined ? true : !!db.get(configPaths.settings.uploadResultNotification)
|
||||
pasteText.push(await pasteTemplate(pasteStyle, imgs[i], db.get(configPaths.settings.customLink)))
|
||||
const isShowResultNotification =
|
||||
db.get(configPaths.settings.uploadResultNotification) === undefined
|
||||
? true
|
||||
: !!db.get(configPaths.settings.uploadResultNotification)
|
||||
if (isShowResultNotification) {
|
||||
const notification = new Notification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
BrowserWindow,
|
||||
clipboard,
|
||||
ipcMain,
|
||||
Notification,
|
||||
WebContents
|
||||
} from 'electron'
|
||||
import { BrowserWindow, clipboard, ipcMain, Notification, WebContents } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import util from 'util'
|
||||
import path from 'path'
|
||||
@@ -21,17 +15,13 @@ import logger from '@core/picgo/logger'
|
||||
import { T } from '~/i18n'
|
||||
import { showNotification, getClipboardFilePath, calcDurationRange } from '~/utils/common'
|
||||
|
||||
import {
|
||||
GET_RENAME_FILE_NAME,
|
||||
RENAME_FILE_NAME,
|
||||
TALKING_DATA_EVENT
|
||||
} from '#/events/constants'
|
||||
import { GET_RENAME_FILE_NAME, RENAME_FILE_NAME, TALKING_DATA_EVENT } from '#/events/constants'
|
||||
import { ICOREBuildInEvent, IWindowList } from '#/types/enum'
|
||||
import { configPaths } from '#/utils/configPaths'
|
||||
import { CLIPBOARD_IMAGE_FOLDER } from '#/utils/static'
|
||||
|
||||
const waitForRename = (window: BrowserWindow, id: number): Promise<string|null> => {
|
||||
return new Promise((resolve) => {
|
||||
const waitForRename = (window: BrowserWindow, id: number): Promise<string | null> => {
|
||||
return new Promise(resolve => {
|
||||
const windowId = window.id
|
||||
ipcMain.once(`${RENAME_FILE_NAME}${id}`, (_: Event, newName: string) => {
|
||||
resolve(newName)
|
||||
@@ -62,11 +52,11 @@ const handleTalkingData = (webContents: WebContents, options: IAnalyticsData) =>
|
||||
class Uploader {
|
||||
private webContents: WebContents | null = null
|
||||
|
||||
constructor () {
|
||||
constructor() {
|
||||
this.init()
|
||||
}
|
||||
|
||||
init () {
|
||||
init() {
|
||||
picgo.on(ICOREBuildInEvent.NOTIFICATION, (message: Electron.NotificationConstructorOptions | undefined) => {
|
||||
const notification = new Notification(message)
|
||||
notification.show()
|
||||
@@ -91,37 +81,39 @@ class Uploader {
|
||||
const rename = db.get(configPaths.settings.rename)
|
||||
const autoRename = db.get(configPaths.settings.autoRename)
|
||||
if (autoRename || rename) {
|
||||
await Promise.all(ctx.output.map(async (item, index) => {
|
||||
let name: undefined | string | null
|
||||
let fileName: string | undefined
|
||||
if (autoRename) {
|
||||
fileName = dayjs().add(index, 'ms').format('YYYYMMDDHHmmSSS') + item.extname
|
||||
} else {
|
||||
fileName = item.fileName
|
||||
}
|
||||
if (rename) {
|
||||
const window = windowManager.create(IWindowList.RENAME_WINDOW)!
|
||||
logger.info('create rename window')
|
||||
ipcMain.on(GET_RENAME_FILE_NAME, (evt) => {
|
||||
try {
|
||||
if (evt.sender.id === window.webContents.id) {
|
||||
logger.info('rename window ready, wait for rename...')
|
||||
window.webContents.send(RENAME_FILE_NAME, fileName, item.fileName, window.webContents.id)
|
||||
await Promise.all(
|
||||
ctx.output.map(async (item, index) => {
|
||||
let name: undefined | string | null
|
||||
let fileName: string | undefined
|
||||
if (autoRename) {
|
||||
fileName = dayjs().add(index, 'ms').format('YYYYMMDDHHmmSSS') + item.extname
|
||||
} else {
|
||||
fileName = item.fileName
|
||||
}
|
||||
if (rename) {
|
||||
const window = windowManager.create(IWindowList.RENAME_WINDOW)!
|
||||
logger.info('create rename window')
|
||||
ipcMain.on(GET_RENAME_FILE_NAME, evt => {
|
||||
try {
|
||||
if (evt.sender.id === window.webContents.id) {
|
||||
logger.info('rename window ready, wait for rename...')
|
||||
window.webContents.send(RENAME_FILE_NAME, fileName, item.fileName, window.webContents.id)
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e)
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e)
|
||||
}
|
||||
})
|
||||
name = await waitForRename(window, window.webContents.id)
|
||||
}
|
||||
item.fileName = name || fileName
|
||||
}))
|
||||
})
|
||||
name = await waitForRename(window, window.webContents.id)
|
||||
}
|
||||
item.fileName = name || fileName
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setWebContents (webContents: WebContents) {
|
||||
setWebContents(webContents: WebContents) {
|
||||
this.webContents = webContents
|
||||
return this
|
||||
}
|
||||
@@ -129,7 +121,7 @@ class Uploader {
|
||||
/**
|
||||
* use electron's clipboard image to upload
|
||||
*/
|
||||
async uploadWithBuildInClipboard (): Promise<ImgInfo[]|false> {
|
||||
async uploadWithBuildInClipboard(): Promise<ImgInfo[] | false> {
|
||||
let filePath = ''
|
||||
try {
|
||||
const imgPath = getClipboardFilePath()
|
||||
@@ -157,7 +149,7 @@ class Uploader {
|
||||
}
|
||||
}
|
||||
|
||||
async upload (img?: IUploadOption): Promise<ImgInfo[]|false> {
|
||||
async upload(img?: IUploadOption): Promise<ImgInfo[] | false> {
|
||||
try {
|
||||
const startTime = Date.now()
|
||||
const output = await picgo.upload(img)
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production'
|
||||
|
||||
export const MANUAL_WINDOW_URL = process.env.NODE_ENV === 'development'
|
||||
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#documents`
|
||||
: 'picgo://./index.html#documents'
|
||||
export const MANUAL_WINDOW_URL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? `${process.env.WEBPACK_DEV_SERVER_URL as string}#documents`
|
||||
: 'picgo://./index.html#documents'
|
||||
|
||||
export const MINI_WINDOW_URL = isDevelopment
|
||||
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#mini-page`
|
||||
? `${process.env.WEBPACK_DEV_SERVER_URL as string}#mini-page`
|
||||
: 'picgo://./index.html#mini-page'
|
||||
|
||||
export const RENAME_WINDOW_URL = process.env.NODE_ENV === 'development'
|
||||
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#rename-page`
|
||||
: 'picgo://./index.html#rename-page'
|
||||
export const RENAME_WINDOW_URL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? `${process.env.WEBPACK_DEV_SERVER_URL as string}#rename-page`
|
||||
: 'picgo://./index.html#rename-page'
|
||||
|
||||
export const SETTING_WINDOW_URL = isDevelopment
|
||||
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#main-page/upload`
|
||||
? `${process.env.WEBPACK_DEV_SERVER_URL as string}#main-page/upload`
|
||||
: 'picgo://./index.html#main-page/upload'
|
||||
|
||||
export const TRAY_WINDOW_URL = isDevelopment
|
||||
? (process.env.WEBPACK_DEV_SERVER_URL as string)
|
||||
: 'picgo://./index.html'
|
||||
export const TRAY_WINDOW_URL = isDevelopment ? (process.env.WEBPACK_DEV_SERVER_URL as string) : 'picgo://./index.html'
|
||||
|
||||
export const TOOLBOX_WINDOW_URL = process.env.NODE_ENV === 'development'
|
||||
? `${(process.env.WEBPACK_DEV_SERVER_URL as string)}#toolbox-page`
|
||||
: 'picgo://./index.html#toolbox-page'
|
||||
export const TOOLBOX_WINDOW_URL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? `${process.env.WEBPACK_DEV_SERVER_URL as string}#toolbox-page`
|
||||
: 'picgo://./index.html#toolbox-page'
|
||||
|
||||
@@ -23,8 +23,11 @@ const windowList = new Map<IWindowList, IWindowListItem>()
|
||||
|
||||
const handleWindowParams = (windowURL: string) => windowURL
|
||||
|
||||
const getDefaultWindowSizes = (): { width: number, height: number } => {
|
||||
const [mainWindowWidth, mainWindowHeight] = db.get([configPaths.settings.mainWindowWidth, configPaths.settings.mainWindowHeight])
|
||||
const getDefaultWindowSizes = (): { width: number; height: number } => {
|
||||
const [mainWindowWidth, mainWindowHeight] = db.get([
|
||||
configPaths.settings.mainWindowWidth,
|
||||
configPaths.settings.mainWindowHeight
|
||||
])
|
||||
return {
|
||||
width: mainWindowWidth || 1200,
|
||||
height: mainWindowHeight || 800
|
||||
@@ -176,7 +179,7 @@ windowList.set(IWindowList.TRAY_WINDOW, {
|
||||
isValid: process.platform !== 'linux',
|
||||
multiple: false,
|
||||
options: () => trayWindowOptions,
|
||||
callback (window) {
|
||||
callback(window) {
|
||||
window.loadURL(handleWindowParams(TRAY_WINDOW_URL))
|
||||
window.on('blur', () => {
|
||||
window.hide()
|
||||
@@ -188,7 +191,7 @@ windowList.set(IWindowList.MANUAL_WINDOW, {
|
||||
isValid: true,
|
||||
multiple: false,
|
||||
options: () => manualWindowOptions,
|
||||
callback (window) {
|
||||
callback(window) {
|
||||
window.loadURL(handleWindowParams(MANUAL_WINDOW_URL))
|
||||
window.focus()
|
||||
}
|
||||
@@ -198,7 +201,7 @@ windowList.set(IWindowList.SETTING_WINDOW, {
|
||||
isValid: true,
|
||||
multiple: false,
|
||||
options: () => settingWindowOptions,
|
||||
callback (window, windowManager) {
|
||||
callback(window, windowManager) {
|
||||
window.loadURL(handleWindowParams(SETTING_WINDOW_URL))
|
||||
window.on('closed', () => {
|
||||
bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, false)
|
||||
@@ -217,7 +220,7 @@ windowList.set(IWindowList.MINI_WINDOW, {
|
||||
isValid: process.platform !== 'darwin',
|
||||
multiple: false,
|
||||
options: () => miniWindowOptions,
|
||||
callback (window) {
|
||||
callback(window) {
|
||||
window.loadURL(handleWindowParams(MINI_WINDOW_URL))
|
||||
}
|
||||
})
|
||||
@@ -226,20 +229,23 @@ windowList.set(IWindowList.RENAME_WINDOW, {
|
||||
isValid: true,
|
||||
multiple: true,
|
||||
options: () => renameWindowOptions,
|
||||
async callback (window, windowManager) {
|
||||
async callback(window, windowManager) {
|
||||
window.loadURL(handleWindowParams(RENAME_WINDOW_URL))
|
||||
const currentWindow = windowManager.getAvailableWindow(true)
|
||||
if (currentWindow && currentWindow.isVisible()) {
|
||||
// bounds: { x: 821, y: 75, width: 800, height: 450 }
|
||||
// bounds: { x: 821, y: 75, width: 800, height: 450 }
|
||||
const bounds = currentWindow.getBounds()
|
||||
const positionX = bounds.x + bounds.width / 2 - 150
|
||||
let positionX = bounds.x + bounds.width / 2 - 150
|
||||
let positionY
|
||||
// if is the settingWindow
|
||||
if (bounds.height > 400) {
|
||||
positionY = bounds.y + bounds.height / 2 - 88
|
||||
} else { // if is the miniWindow
|
||||
} else {
|
||||
// if is the miniWindow
|
||||
positionY = bounds.y + bounds.height / 2
|
||||
}
|
||||
positionX = Math.floor(positionX)
|
||||
positionY = Math.floor(positionY)
|
||||
window.setPosition(positionX, positionY, false)
|
||||
}
|
||||
}
|
||||
@@ -249,18 +255,20 @@ windowList.set(IWindowList.TOOLBOX_WINDOW, {
|
||||
isValid: true,
|
||||
multiple: false,
|
||||
options: () => toolboxWindowOptions,
|
||||
async callback (window, windowManager) {
|
||||
async callback(window, windowManager) {
|
||||
window.loadURL(TOOLBOX_WINDOW_URL)
|
||||
const currentWindow = windowManager.getAvailableWindow(true)
|
||||
if (currentWindow && currentWindow.isVisible()) {
|
||||
const bounds = currentWindow.getBounds()
|
||||
const positionX = bounds.x + bounds.width / 2 - 400
|
||||
let positionX = bounds.x + bounds.width / 2 - 400
|
||||
let positionY
|
||||
if (bounds.height > 400) {
|
||||
positionY = bounds.y + bounds.height / 2 - 225
|
||||
} else {
|
||||
positionY = bounds.y + bounds.height / 2
|
||||
}
|
||||
positionX = Math.floor(positionX)
|
||||
positionY = Math.floor(positionY)
|
||||
window.setPosition(positionX, positionY, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ class WindowManager implements IWindowManager {
|
||||
#windowMap: Map<IWindowList | string, BrowserWindow> = new Map()
|
||||
#windowIdMap: Map<number, IWindowList | string> = new Map()
|
||||
|
||||
create (name: IWindowList) {
|
||||
create(name: IWindowList) {
|
||||
const windowConfig: IWindowListItem = windowList.get(name)!
|
||||
if (windowConfig.isValid) {
|
||||
if (!windowConfig.multiple) {
|
||||
@@ -32,7 +32,7 @@ class WindowManager implements IWindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
get (name: IWindowList) {
|
||||
get(name: IWindowList) {
|
||||
if (this.has(name)) {
|
||||
return this.#windowMap.get(name)!
|
||||
} else {
|
||||
@@ -41,7 +41,7 @@ class WindowManager implements IWindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
has (name: IWindowList) {
|
||||
has(name: IWindowList) {
|
||||
return this.#windowMap.has(name)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class WindowManager implements IWindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
getAvailableWindow (isSkipMiniWindow = false) {
|
||||
getAvailableWindow(isSkipMiniWindow = false) {
|
||||
const miniWindow = this.#windowMap.get(IWindowList.MINI_WINDOW)
|
||||
if (miniWindow && miniWindow.isVisible() && !isSkipMiniWindow) {
|
||||
return miniWindow
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
} from '@core/bus/constants'
|
||||
|
||||
export const uploadWithClipboardFiles = (): Promise<{
|
||||
success: boolean,
|
||||
success: boolean
|
||||
result?: string[]
|
||||
}> => {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
bus.once(UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE, (result: string) => {
|
||||
if (result) {
|
||||
return resolve({
|
||||
@@ -31,11 +31,13 @@ export const uploadWithClipboardFiles = (): Promise<{
|
||||
})
|
||||
}
|
||||
|
||||
export const uploadWithFiles = (pathList: IFileWithPath[]): Promise<{
|
||||
success: boolean,
|
||||
export const uploadWithFiles = (
|
||||
pathList: IFileWithPath[]
|
||||
): Promise<{
|
||||
success: boolean
|
||||
result?: string[]
|
||||
}> => {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
bus.once(UPLOAD_WITH_FILES_RESPONSE, (result: string[]) => {
|
||||
if (result.length) {
|
||||
return resolve({
|
||||
@@ -55,7 +57,7 @@ export const uploadWithFiles = (pathList: IFileWithPath[]): Promise<{
|
||||
// get available window id:
|
||||
// miniWindow or settingWindow or trayWindow
|
||||
export const getWindowId = (): Promise<number> => {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
bus.once(GET_WINDOW_ID_REPONSE, (id: number) => {
|
||||
resolve(id)
|
||||
})
|
||||
@@ -65,7 +67,7 @@ export const getWindowId = (): Promise<number> => {
|
||||
|
||||
// get settingWindow id:
|
||||
export const getSettingWindowId = (): Promise<number> => {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
bus.once(GET_SETTING_WINDOW_ID_RESPONSE, (id: number) => {
|
||||
resolve(id)
|
||||
})
|
||||
|
||||
@@ -25,7 +25,7 @@ const errorMsg = {
|
||||
/** ensure notification list */
|
||||
if (!global.notificationList) global.notificationList = []
|
||||
|
||||
function dbChecker () {
|
||||
function dbChecker() {
|
||||
if (process.type !== 'renderer') {
|
||||
// db save bak
|
||||
try {
|
||||
@@ -54,7 +54,9 @@ function dbChecker () {
|
||||
fs.unlinkSync(configFilePath)
|
||||
if (fs.existsSync(configFileBackupPath)) {
|
||||
try {
|
||||
configFile = fs.readFileSync(configFileBackupPath, { encoding: 'utf-8' })
|
||||
configFile = fs.readFileSync(configFileBackupPath, {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
JSON.parse(configFile)
|
||||
writeFile.sync(configFilePath, configFile, { encoding: 'utf-8' })
|
||||
const stats = fs.statSync(configFileBackupPath)
|
||||
@@ -80,7 +82,7 @@ function dbChecker () {
|
||||
/**
|
||||
* Get config path
|
||||
*/
|
||||
function dbPathChecker (): string {
|
||||
function dbPathChecker(): string {
|
||||
if (_configFilePath) {
|
||||
return _configFilePath
|
||||
}
|
||||
@@ -91,7 +93,9 @@ function dbPathChecker (): string {
|
||||
return _configFilePath
|
||||
}
|
||||
try {
|
||||
const configString = fs.readFileSync(defaultConfigPath, { encoding: 'utf-8' })
|
||||
const configString = fs.readFileSync(defaultConfigPath, {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
const config = JSON.parse(configString)
|
||||
const userConfigPath: string = config.configPath || ''
|
||||
if (userConfigPath) {
|
||||
@@ -118,11 +122,11 @@ function dbPathChecker (): string {
|
||||
}
|
||||
}
|
||||
|
||||
function dbPathDir () {
|
||||
function dbPathDir() {
|
||||
return path.dirname(dbPathChecker())
|
||||
}
|
||||
|
||||
function getGalleryDBPath (): {
|
||||
function getGalleryDBPath(): {
|
||||
dbPath: string
|
||||
dbBackupPath: string
|
||||
} {
|
||||
@@ -135,9 +139,4 @@ function getGalleryDBPath (): {
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
dbChecker,
|
||||
dbPathChecker,
|
||||
dbPathDir,
|
||||
getGalleryDBPath
|
||||
}
|
||||
export { dbChecker, dbPathChecker, dbPathDir, getGalleryDBPath }
|
||||
|
||||
@@ -19,7 +19,7 @@ export const DB_PATH: string = getGalleryDBPath().dbPath
|
||||
class ConfigStore {
|
||||
#db: JSONStore
|
||||
|
||||
constructor () {
|
||||
constructor() {
|
||||
this.#db = new JSONStore(CONFIG_PATH)
|
||||
|
||||
if (!this.#db.has('picBed')) {
|
||||
@@ -43,11 +43,11 @@ class ConfigStore {
|
||||
this.read()
|
||||
}
|
||||
|
||||
read (flush?: boolean): IJSON {
|
||||
read(flush?: boolean): IJSON {
|
||||
return this.#db.read(flush)
|
||||
}
|
||||
|
||||
getSingle (key = ''): any {
|
||||
getSingle(key = ''): any {
|
||||
if (key === '') {
|
||||
return this.#db.read(true)
|
||||
}
|
||||
@@ -55,43 +55,43 @@ class ConfigStore {
|
||||
return this.#db.get(key)
|
||||
}
|
||||
|
||||
get (key: string): any
|
||||
get (key: string[]): any[]
|
||||
get (key: string | string[] = ''): any {
|
||||
get(key: string): any
|
||||
get(key: string[]): any[]
|
||||
get(key: string | string[] = ''): any {
|
||||
if (Array.isArray(key)) {
|
||||
return key.map(k => this.getSingle(k))
|
||||
}
|
||||
return this.getSingle(key)
|
||||
}
|
||||
|
||||
set (key: string, value: any): void {
|
||||
set(key: string, value: any): void {
|
||||
this.read(true)
|
||||
return this.#db.set(key, value)
|
||||
}
|
||||
|
||||
has (key: string) {
|
||||
has(key: string) {
|
||||
this.read(true)
|
||||
return this.#db.has(key)
|
||||
}
|
||||
|
||||
unset (key: string, value: any): boolean {
|
||||
unset(key: string, value: any): boolean {
|
||||
this.read(true)
|
||||
return this.#db.unset(key, value)
|
||||
}
|
||||
|
||||
saveConfig (config: Partial<IConfig>): void {
|
||||
saveConfig(config: Partial<IConfig>): void {
|
||||
Object.keys(config).forEach((name: string) => {
|
||||
this.set(name, config[name])
|
||||
})
|
||||
}
|
||||
|
||||
removeConfig (config: IConfig): void {
|
||||
removeConfig(config: IConfig): void {
|
||||
Object.keys(config).forEach((name: string) => {
|
||||
this.unset(name, config[name])
|
||||
})
|
||||
}
|
||||
|
||||
getConfigPath () {
|
||||
getConfigPath() {
|
||||
return CONFIG_PATH
|
||||
}
|
||||
}
|
||||
@@ -103,11 +103,11 @@ export default db
|
||||
// v2.3.0 add gallery db
|
||||
class GalleryDB {
|
||||
static #instance: DBStore
|
||||
private constructor () {
|
||||
private constructor() {
|
||||
console.log('init gallery db')
|
||||
}
|
||||
|
||||
static getInstance (): DBStore {
|
||||
static getInstance(): DBStore {
|
||||
if (!GalleryDB.#instance) {
|
||||
GalleryDB.#instance = new DBStore(DB_PATH, 'gallery')
|
||||
}
|
||||
@@ -115,6 +115,4 @@ class GalleryDB {
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
GalleryDB
|
||||
}
|
||||
export { GalleryDB }
|
||||
|
||||
@@ -21,7 +21,7 @@ picgo.GUI_VERSION = global.PICGO_GUI_VERSION
|
||||
|
||||
const originPicGoSaveConfig = picgo.saveConfig.bind(picgo)
|
||||
|
||||
function flushDB () {
|
||||
function flushDB() {
|
||||
db.read(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,4 @@ const getLogger = (logPath: string, logType: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getLogger
|
||||
}
|
||||
export { getLogger }
|
||||
|
||||
+71
-66
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
BrowserWindow,
|
||||
dialog,
|
||||
ipcMain,
|
||||
Notification
|
||||
} from 'electron'
|
||||
import { BrowserWindow, dialog, ipcMain, Notification } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { DBStore } from '@picgo/store'
|
||||
@@ -29,55 +24,57 @@ class GuiApi implements IGuiApi {
|
||||
private static instance: GuiApi
|
||||
private windowId: number = -1
|
||||
private settingWindowId: number = -1
|
||||
private constructor () {
|
||||
private constructor() {
|
||||
console.log('init guiapi')
|
||||
}
|
||||
|
||||
static getInstance (): GuiApi {
|
||||
static getInstance(): GuiApi {
|
||||
if (!GuiApi.instance) {
|
||||
GuiApi.instance = new GuiApi()
|
||||
}
|
||||
return GuiApi.instance
|
||||
}
|
||||
|
||||
private async showSettingWindow () {
|
||||
private async showSettingWindow() {
|
||||
this.settingWindowId = await getSettingWindowId()
|
||||
const settingWindow = BrowserWindow.fromId(this.settingWindowId)
|
||||
if (settingWindow?.isVisible()) {
|
||||
return true
|
||||
}
|
||||
settingWindow?.show()
|
||||
return new Promise<void>((resolve) => {
|
||||
return new Promise<void>(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
}, 1000) // TODO: a better way to wait page loaded.
|
||||
})
|
||||
}
|
||||
|
||||
private getWebcontentsByWindowId (id: number) {
|
||||
private getWebcontentsByWindowId(id: number) {
|
||||
return BrowserWindow.fromId(id)?.webContents
|
||||
}
|
||||
|
||||
async showInputBox (options: IShowInputBoxOption = {
|
||||
title: '',
|
||||
placeholder: ''
|
||||
}) {
|
||||
async showInputBox(
|
||||
options: IShowInputBoxOption = {
|
||||
title: '',
|
||||
placeholder: ''
|
||||
}
|
||||
) {
|
||||
await this.showSettingWindow()
|
||||
this.getWebcontentsByWindowId(this.settingWindowId)?.send(SHOW_INPUT_BOX, options)
|
||||
return new Promise<string>((resolve) => {
|
||||
return new Promise<string>(resolve => {
|
||||
ipcMain.once(SHOW_INPUT_BOX, (_: Event, value: string) => {
|
||||
resolve(value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async showFileExplorer (options: IShowFileExplorerOption = {}) {
|
||||
async showFileExplorer(options: IShowFileExplorerOption = {}) {
|
||||
this.windowId = await getWindowId()
|
||||
const res = await dialog.showOpenDialog(BrowserWindow.fromId(this.windowId)!, options)
|
||||
return res.filePaths || []
|
||||
}
|
||||
|
||||
async upload (input: IUploadOption) {
|
||||
async upload(input: IUploadOption) {
|
||||
this.windowId = await getWindowId()
|
||||
const webContents = this.getWebcontentsByWindowId(this.windowId)
|
||||
const rawInput = cloneDeep(input)
|
||||
@@ -90,8 +87,11 @@ class GuiApi implements IGuiApi {
|
||||
if (deleteLocalFile) {
|
||||
await fs.remove(rawInput[i])
|
||||
}
|
||||
pasteText.push(await (pasteTemplate(pasteStyle, imgs[i], db.get(configPaths.settings.customLink))))
|
||||
const isShowResultNotification = db.get(configPaths.settings.uploadResultNotification) === undefined ? true : !!db.get(configPaths.settings.uploadResultNotification)
|
||||
pasteText.push(await pasteTemplate(pasteStyle, imgs[i], db.get(configPaths.settings.customLink)))
|
||||
const isShowResultNotification =
|
||||
db.get(configPaths.settings.uploadResultNotification) === undefined
|
||||
? true
|
||||
: !!db.get(configPaths.settings.uploadResultNotification)
|
||||
if (isShowResultNotification) {
|
||||
const notification = new Notification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
@@ -112,10 +112,12 @@ class GuiApi implements IGuiApi {
|
||||
return []
|
||||
}
|
||||
|
||||
showNotification (options: IShowNotificationOption = {
|
||||
title: '',
|
||||
body: ''
|
||||
}) {
|
||||
showNotification(
|
||||
options: IShowNotificationOption = {
|
||||
title: '',
|
||||
body: ''
|
||||
}
|
||||
) {
|
||||
const notification = new Notification({
|
||||
title: options.title,
|
||||
body: options.body
|
||||
@@ -123,18 +125,17 @@ class GuiApi implements IGuiApi {
|
||||
notification.show()
|
||||
}
|
||||
|
||||
showMessageBox (options: IShowMessageBoxOption = {
|
||||
title: '',
|
||||
message: '',
|
||||
type: 'info',
|
||||
buttons: ['Yes', 'No']
|
||||
}) {
|
||||
return new Promise<IShowMessageBoxResult>(async (resolve) => {
|
||||
showMessageBox(
|
||||
options: IShowMessageBoxOption = {
|
||||
title: '',
|
||||
message: '',
|
||||
type: 'info',
|
||||
buttons: ['Yes', 'No']
|
||||
}
|
||||
) {
|
||||
return new Promise<IShowMessageBoxResult>(async resolve => {
|
||||
this.windowId = await getWindowId()
|
||||
dialog.showMessageBox(
|
||||
BrowserWindow.fromId(this.windowId)!,
|
||||
options
|
||||
).then((res) => {
|
||||
dialog.showMessageBox(BrowserWindow.fromId(this.windowId)!, options).then(res => {
|
||||
resolve({
|
||||
result: res.response,
|
||||
checkboxChecked: res.checkboxChecked
|
||||
@@ -146,7 +147,7 @@ class GuiApi implements IGuiApi {
|
||||
/**
|
||||
* get picgo config/data path
|
||||
*/
|
||||
async getConfigPath () {
|
||||
async getConfigPath() {
|
||||
const currentConfigPath = dbPathChecker()
|
||||
const galleryDBPath = getGalleryDBPath().dbPath
|
||||
return {
|
||||
@@ -156,47 +157,51 @@ class GuiApi implements IGuiApi {
|
||||
}
|
||||
}
|
||||
|
||||
get galleryDB (): DBStore {
|
||||
get galleryDB(): DBStore {
|
||||
return new Proxy<DBStore>(GalleryDB.getInstance(), {
|
||||
get (target, prop: keyof DBStore) {
|
||||
get(target, prop: keyof DBStore) {
|
||||
if (prop === 'overwrite') {
|
||||
return new Proxy(GalleryDB.getInstance().overwrite, {
|
||||
apply (target, ctx, args) {
|
||||
return new Promise((resolve) => {
|
||||
apply(target, ctx, args) {
|
||||
return new Promise(resolve => {
|
||||
const guiApi = GuiApi.getInstance()
|
||||
guiApi.showMessageBox({
|
||||
title: T('TIPS_WARNING'),
|
||||
message: T('TIPS_PLUGIN_REMOVE_GALLERY_ITEM'),
|
||||
type: 'info',
|
||||
buttons: ['Yes', 'No']
|
||||
}).then(res => {
|
||||
if (res.result === 0) {
|
||||
resolve(Reflect.apply(target, ctx, args))
|
||||
} else {
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
guiApi
|
||||
.showMessageBox({
|
||||
title: T('TIPS_WARNING'),
|
||||
message: T('TIPS_PLUGIN_REMOVE_GALLERY_ITEM'),
|
||||
type: 'info',
|
||||
buttons: ['Yes', 'No']
|
||||
})
|
||||
.then(res => {
|
||||
if (res.result === 0) {
|
||||
resolve(Reflect.apply(target, ctx, args))
|
||||
} else {
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
if (prop === 'removeById') {
|
||||
return new Proxy(GalleryDB.getInstance().removeById, {
|
||||
apply (target, ctx, args) {
|
||||
return new Promise((resolve) => {
|
||||
apply(target, ctx, args) {
|
||||
return new Promise(resolve => {
|
||||
const guiApi = GuiApi.getInstance()
|
||||
guiApi.showMessageBox({
|
||||
title: T('TIPS_WARNING'),
|
||||
message: T('TIPS_PLUGIN_REMOVE_GALLERY_ITEM'),
|
||||
type: 'info',
|
||||
buttons: ['Yes', 'No']
|
||||
}).then(res => {
|
||||
if (res.result === 0) {
|
||||
resolve(Reflect.apply(target, ctx, args))
|
||||
} else {
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
guiApi
|
||||
.showMessageBox({
|
||||
title: T('TIPS_WARNING'),
|
||||
message: T('TIPS_PLUGIN_REMOVE_GALLERY_ITEM'),
|
||||
type: 'info',
|
||||
buttons: ['Yes', 'No']
|
||||
})
|
||||
.then(res => {
|
||||
if (res.result === 0) {
|
||||
resolve(Reflect.apply(target, ctx, args))
|
||||
} else {
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -11,18 +11,13 @@ import {
|
||||
UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE
|
||||
} from '@core/bus/constants'
|
||||
|
||||
import {
|
||||
createMenu
|
||||
} from 'apis/app/system'
|
||||
import {
|
||||
uploadChoosedFiles,
|
||||
uploadClipboardFiles
|
||||
} from 'apis/app/uploader/apis'
|
||||
import { createMenu } from 'apis/app/system'
|
||||
import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
|
||||
import { IWindowList } from '#/types/enum'
|
||||
|
||||
function initEventCenter () {
|
||||
function initEventCenter() {
|
||||
const eventList: any = {
|
||||
'picgo:upload': uploadClipboardFiles,
|
||||
[UPLOAD_WITH_CLIPBOARD_FILES]: busCallUploadClipboardFiles,
|
||||
@@ -36,31 +31,31 @@ function initEventCenter () {
|
||||
}
|
||||
}
|
||||
|
||||
async function busCallUploadClipboardFiles () {
|
||||
async function busCallUploadClipboardFiles() {
|
||||
const result = await uploadClipboardFiles()
|
||||
const imgUrl = result.url
|
||||
bus.emit(UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE, imgUrl)
|
||||
}
|
||||
|
||||
async function busCallUploadFiles (pathList: IFileWithPath[]) {
|
||||
async function busCallUploadFiles(pathList: IFileWithPath[]) {
|
||||
const win = windowManager.getAvailableWindow()
|
||||
const result = await uploadChoosedFiles(win.webContents, pathList)
|
||||
const urls = result.map((item: any) => item.url)
|
||||
bus.emit(UPLOAD_WITH_FILES_RESPONSE, urls)
|
||||
}
|
||||
|
||||
function busCallGetWindowId () {
|
||||
function busCallGetWindowId() {
|
||||
const win = windowManager.getAvailableWindow()
|
||||
bus.emit(GET_WINDOW_ID_REPONSE, win.id)
|
||||
}
|
||||
|
||||
function busCallGetSettingWindowId () {
|
||||
function busCallGetSettingWindowId() {
|
||||
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
bus.emit(GET_SETTING_WINDOW_ID_RESPONSE, settingWindow.id)
|
||||
}
|
||||
|
||||
export default {
|
||||
listen () {
|
||||
listen() {
|
||||
initEventCenter()
|
||||
}
|
||||
}
|
||||
|
||||
+112
-120
@@ -1,20 +1,10 @@
|
||||
import {
|
||||
app,
|
||||
dialog,
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
shell,
|
||||
MenuItemConstructorOptions,
|
||||
MenuItem
|
||||
} from 'electron'
|
||||
import { app, dialog, BrowserWindow, Menu, shell, MenuItemConstructorOptions, MenuItem } from 'electron'
|
||||
import { PicGo as PicGoCore } from 'piclist'
|
||||
|
||||
import db from '@core/datastore'
|
||||
import picgo from '@core/picgo'
|
||||
|
||||
import {
|
||||
uploadClipboardFiles
|
||||
} from 'apis/app/uploader/apis'
|
||||
import { uploadClipboardFiles } from 'apis/app/uploader/apis'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
import GuiApi from 'apis/gui'
|
||||
|
||||
@@ -47,7 +37,7 @@ const buildMiniPageMenu = () => {
|
||||
const isListeningClipboard = db.get(configPaths.settings.isListeningClipboard) || false
|
||||
const ClipboardWatcher = clipboardPoll
|
||||
const submenu = buildPicBedListMenu()
|
||||
const template: Array<(MenuItemConstructorOptions) | (MenuItem)> = [
|
||||
const template: Array<MenuItemConstructorOptions | MenuItem> = [
|
||||
{
|
||||
label: T('OPEN_MAIN_WINDOW'),
|
||||
click: openMainWindow
|
||||
@@ -59,19 +49,19 @@ const buildMiniPageMenu = () => {
|
||||
},
|
||||
{
|
||||
label: T('UPLOAD_BY_CLIPBOARD'),
|
||||
click () {
|
||||
click() {
|
||||
uploadClipboardFiles()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: T('HIDE_MINI_WINDOW'),
|
||||
click () {
|
||||
click() {
|
||||
BrowserWindow.getFocusedWindow()!.hide()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: T('START_WATCH_CLIPBOARD'),
|
||||
click () {
|
||||
click() {
|
||||
db.set(configPaths.settings.isListeningClipboard, true)
|
||||
ClipboardWatcher.startListening()
|
||||
ClipboardWatcher.on('change', () => {
|
||||
@@ -84,7 +74,7 @@ const buildMiniPageMenu = () => {
|
||||
},
|
||||
{
|
||||
label: T('STOP_WATCH_CLIPBOARD'),
|
||||
click () {
|
||||
click() {
|
||||
db.set(configPaths.settings.isListeningClipboard, false)
|
||||
ClipboardWatcher.stopListening()
|
||||
ClipboardWatcher.removeAllListeners()
|
||||
@@ -94,7 +84,7 @@ const buildMiniPageMenu = () => {
|
||||
},
|
||||
{
|
||||
label: T('RELOAD_APP'),
|
||||
click () {
|
||||
click() {
|
||||
app.relaunch()
|
||||
app.exit(0)
|
||||
}
|
||||
@@ -111,7 +101,7 @@ const buildMainPageMenu = (win: BrowserWindow) => {
|
||||
const template = [
|
||||
{
|
||||
label: T('ABOUT'),
|
||||
click () {
|
||||
click() {
|
||||
dialog.showMessageBox({
|
||||
title: 'PicList',
|
||||
message: 'PicList',
|
||||
@@ -121,32 +111,31 @@ const buildMainPageMenu = (win: BrowserWindow) => {
|
||||
},
|
||||
{
|
||||
label: T('SHOW_PICBED_QRCODE'),
|
||||
click () {
|
||||
click() {
|
||||
win?.webContents?.send(SHOW_MAIN_PAGE_QRCODE)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: T('OPEN_TOOLBOX'),
|
||||
click () {
|
||||
click() {
|
||||
const window = windowManager.create(IWindowList.TOOLBOX_WINDOW)
|
||||
window?.show()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: T('SHOW_DEVTOOLS'),
|
||||
click () {
|
||||
click() {
|
||||
win?.webContents?.openDevTools({ mode: 'detach' })
|
||||
}
|
||||
},
|
||||
{
|
||||
label: T('FEEDBACK'),
|
||||
click () {
|
||||
click() {
|
||||
const url = 'https://github.com/Kuingsmile/PicList/issues'
|
||||
shell.openExternal(url)
|
||||
}
|
||||
}
|
||||
]
|
||||
// @ts-ignore
|
||||
] as Array<MenuItemConstructorOptions | MenuItem>
|
||||
return Menu.buildFromTemplate(template)
|
||||
}
|
||||
|
||||
@@ -155,55 +144,60 @@ const buildPicBedListMenu = () => {
|
||||
const currentPicBed = picgo.getConfig(configPaths.picBed.uploader)
|
||||
const currentPicBedName = picBeds.find(item => item.type === currentPicBed)?.name
|
||||
const picBedConfigList = picgo.getConfig<IUploaderConfig>('uploader')
|
||||
const currentPicBedMenuItem = [{
|
||||
label: `${T('CURRENT_PICBED')} - ${currentPicBedName}`,
|
||||
enabled: false
|
||||
}, {
|
||||
type: 'separator'
|
||||
}]
|
||||
let submenu = picBeds.filter(item => item.visible).map(item => {
|
||||
const configList = picBedConfigList?.[item.type]?.configList
|
||||
const defaultId = picBedConfigList?.[item.type]?.defaultId
|
||||
const hasSubmenu = !!configList
|
||||
return {
|
||||
label: item.name,
|
||||
type: !hasSubmenu ? 'checkbox' : undefined,
|
||||
checked: !hasSubmenu ? (currentPicBed === item.type) : undefined,
|
||||
submenu: hasSubmenu
|
||||
? configList.map((config) => {
|
||||
return {
|
||||
label: config._configName || 'Default',
|
||||
// if only one config, use checkbox, or radio will checked as default
|
||||
// see: https://github.com/electron/electron/issues/21292
|
||||
type: 'checkbox',
|
||||
checked: config._id === defaultId && (item.type === currentPicBed),
|
||||
click: function () {
|
||||
changeCurrentUploader(item.type, config, config._id)
|
||||
const currentPicBedMenuItem = [
|
||||
{
|
||||
label: `${T('CURRENT_PICBED')} - ${currentPicBedName}`,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
}
|
||||
]
|
||||
let submenu = picBeds
|
||||
.filter(item => item.visible)
|
||||
.map(item => {
|
||||
const configList = picBedConfigList?.[item.type]?.configList
|
||||
const defaultId = picBedConfigList?.[item.type]?.defaultId
|
||||
const hasSubmenu = !!configList
|
||||
return {
|
||||
label: item.name,
|
||||
type: !hasSubmenu ? 'checkbox' : undefined,
|
||||
checked: !hasSubmenu ? currentPicBed === item.type : undefined,
|
||||
submenu: hasSubmenu
|
||||
? configList.map(config => {
|
||||
return {
|
||||
label: config._configName || 'Default',
|
||||
// if only one config, use checkbox, or radio will checked as default
|
||||
// see: https://github.com/electron/electron/issues/21292
|
||||
type: 'checkbox',
|
||||
checked: config._id === defaultId && item.type === currentPicBed,
|
||||
click: function () {
|
||||
changeCurrentUploader(item.type, config, config._id)
|
||||
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
|
||||
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')
|
||||
}
|
||||
setTrayToolTip(`${item.type} ${config._configName || 'Default'}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
: undefined,
|
||||
click: !hasSubmenu
|
||||
? function () {
|
||||
picgo.saveConfig({
|
||||
[configPaths.picBed.current]: item.type,
|
||||
[configPaths.picBed.uploader]: item.type
|
||||
})
|
||||
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
|
||||
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')
|
||||
}
|
||||
setTrayToolTip(`${item.type} ${config._configName || 'Default'}`)
|
||||
setTrayToolTip(item.type)
|
||||
}
|
||||
}
|
||||
})
|
||||
: undefined,
|
||||
click: !hasSubmenu
|
||||
? function () {
|
||||
picgo.saveConfig({
|
||||
[configPaths.picBed.current]: item.type,
|
||||
[configPaths.picBed.uploader]: item.type
|
||||
})
|
||||
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
|
||||
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents.send('syncPicBed')
|
||||
}
|
||||
setTrayToolTip(item.type)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
// @ts-ignore
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
// @ts-expect-error submenu type
|
||||
submenu = currentPicBedMenuItem.concat(submenu)
|
||||
// @ts-ignore
|
||||
// @ts-expect-error submenu type
|
||||
return Menu.buildFromTemplate(submenu)
|
||||
}
|
||||
|
||||
@@ -230,56 +224,61 @@ const handleRestoreState = (item: string, name: string): void => {
|
||||
}
|
||||
|
||||
const buildPluginPageMenu = (plugin: IPicGoPlugin) => {
|
||||
const menu = [{
|
||||
label: T('ENABLE_PLUGIN'),
|
||||
enabled: !plugin.enabled,
|
||||
click () {
|
||||
picgo.saveConfig({
|
||||
[`picgoPlugins.${plugin.fullName}`]: true
|
||||
})
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
window.webContents.send(PICGO_TOGGLE_PLUGIN, plugin.fullName, true)
|
||||
}
|
||||
}, {
|
||||
label: T('DISABLE_PLUGIN'),
|
||||
enabled: plugin.enabled,
|
||||
click () {
|
||||
picgo.saveConfig({
|
||||
[`picgoPlugins.${plugin.fullName}`]: false
|
||||
})
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
window.webContents.send(PICGO_HANDLE_PLUGIN_ING, plugin.fullName)
|
||||
window.webContents.send(PICGO_TOGGLE_PLUGIN, plugin.fullName, false)
|
||||
window.webContents.send(PICGO_HANDLE_PLUGIN_DONE, plugin.fullName)
|
||||
if (plugin.config.transformer.name) {
|
||||
handleRestoreState('transformer', plugin.config.transformer.name)
|
||||
const menu = [
|
||||
{
|
||||
label: T('ENABLE_PLUGIN'),
|
||||
enabled: !plugin.enabled,
|
||||
click() {
|
||||
picgo.saveConfig({
|
||||
[`picgoPlugins.${plugin.fullName}`]: true
|
||||
})
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
window.webContents.send(PICGO_TOGGLE_PLUGIN, plugin.fullName, true)
|
||||
}
|
||||
if (plugin.config.uploader.name) {
|
||||
handleRestoreState('uploader', plugin.config.uploader.name)
|
||||
},
|
||||
{
|
||||
label: T('DISABLE_PLUGIN'),
|
||||
enabled: plugin.enabled,
|
||||
click() {
|
||||
picgo.saveConfig({
|
||||
[`picgoPlugins.${plugin.fullName}`]: false
|
||||
})
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
window.webContents.send(PICGO_HANDLE_PLUGIN_ING, plugin.fullName)
|
||||
window.webContents.send(PICGO_TOGGLE_PLUGIN, plugin.fullName, false)
|
||||
window.webContents.send(PICGO_HANDLE_PLUGIN_DONE, plugin.fullName)
|
||||
if (plugin.config.transformer.name) {
|
||||
handleRestoreState('transformer', plugin.config.transformer.name)
|
||||
}
|
||||
if (plugin.config.uploader.name) {
|
||||
handleRestoreState('uploader', plugin.config.uploader.name)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: T('UNINSTALL_PLUGIN'),
|
||||
click() {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
window.webContents.send(PICGO_HANDLE_PLUGIN_ING, plugin.fullName)
|
||||
handlePluginUninstall(plugin.fullName)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: T('UPDATE_PLUGIN'),
|
||||
click() {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
window.webContents.send(PICGO_HANDLE_PLUGIN_ING, plugin.fullName)
|
||||
handlePluginUpdate(plugin.fullName)
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: T('UNINSTALL_PLUGIN'),
|
||||
click () {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
window.webContents.send(PICGO_HANDLE_PLUGIN_ING, plugin.fullName)
|
||||
handlePluginUninstall(plugin.fullName)
|
||||
}
|
||||
}, {
|
||||
label: T('UPDATE_PLUGIN'),
|
||||
click () {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
window.webContents.send(PICGO_HANDLE_PLUGIN_ING, plugin.fullName)
|
||||
handlePluginUpdate(plugin.fullName)
|
||||
}
|
||||
}]
|
||||
] as Array<MenuItemConstructorOptions | MenuItem>
|
||||
for (const i in plugin.config) {
|
||||
if (plugin.config[i].config.length > 0) {
|
||||
const obj = {
|
||||
label: T('CONFIG_THING', {
|
||||
c: `${i} - ${plugin.config[i].fullName || plugin.config[i].name}`
|
||||
}),
|
||||
click () {
|
||||
click() {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const currentType = i
|
||||
const configName = plugin.config[i].fullName || plugin.config[i].name
|
||||
@@ -297,7 +296,7 @@ const buildPluginPageMenu = (plugin: IPicGoPlugin) => {
|
||||
const pluginTransformer = plugin.config.transformer.name
|
||||
const obj = {
|
||||
label: `${currentTransformer === pluginTransformer ? T('DISABLE') : T('ENABLE')}transformer - ${plugin.config.transformer.name}`,
|
||||
click () {
|
||||
click() {
|
||||
const transformer = plugin.config.transformer.name
|
||||
const currentTransformer = picgo.getConfig<string>(configPaths.picBed.transformer) || 'path'
|
||||
if (currentTransformer === transformer) {
|
||||
@@ -317,13 +316,12 @@ const buildPluginPageMenu = (plugin: IPicGoPlugin) => {
|
||||
// plugin custom menus
|
||||
if (plugin.guiMenu) {
|
||||
menu.push({
|
||||
// @ts-ignore
|
||||
type: 'separator'
|
||||
})
|
||||
for (const i of plugin.guiMenu) {
|
||||
menu.push({
|
||||
label: i.label,
|
||||
click () {
|
||||
click() {
|
||||
const picgPlugin = picgo.pluginLoader.getPlugin(plugin.fullName)
|
||||
if (picgPlugin?.guiMenu?.(picgo)?.length) {
|
||||
const menu: GuiMenuItem[] = picgPlugin.guiMenu(picgo)
|
||||
@@ -338,13 +336,7 @@ const buildPluginPageMenu = (plugin: IPicGoPlugin) => {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return Menu.buildFromTemplate(menu)
|
||||
}
|
||||
|
||||
export {
|
||||
buildMiniPageMenu,
|
||||
buildMainPageMenu,
|
||||
buildPicBedListMenu,
|
||||
buildPluginPageMenu
|
||||
}
|
||||
export { buildMiniPageMenu, buildMainPageMenu, buildPicBedListMenu, buildPluginPageMenu }
|
||||
|
||||
@@ -10,6 +10,7 @@ import { systemRouter } from '~/events/rpc/routes/system'
|
||||
import { toolboxRouter } from '~/events/rpc/routes/toolbox'
|
||||
import { trayRouter } from '~/events/rpc/routes/tray'
|
||||
import { uploadRouter } from '~/events/rpc/routes/upload'
|
||||
import { manageRouter } from '~/events/rpc/routes/manage'
|
||||
|
||||
import { IRPCActionType, IRPCType } from '#/types/enum'
|
||||
import { RPC_ACTIONS, RPC_ACTIONS_INVOKE } from '#/events/constants'
|
||||
@@ -45,12 +46,12 @@ class RPCServer implements IRPCServer {
|
||||
}
|
||||
}
|
||||
|
||||
start () {
|
||||
start() {
|
||||
ipcMain.on(RPC_ACTIONS, this.rpcEventHandler)
|
||||
ipcMain.handle(RPC_ACTIONS_INVOKE, this.rpcEventHandlerWithResponse)
|
||||
}
|
||||
|
||||
use (routes: IRPCRoutes) {
|
||||
use(routes: IRPCRoutes) {
|
||||
for (const [action, route] of routes) {
|
||||
if (route.type === IRPCType.SEND) {
|
||||
this.routes.set(action, route)
|
||||
@@ -60,7 +61,7 @@ class RPCServer implements IRPCServer {
|
||||
}
|
||||
}
|
||||
|
||||
stop () {
|
||||
stop() {
|
||||
ipcMain.off(RPC_ACTIONS, this.rpcEventHandler)
|
||||
}
|
||||
}
|
||||
@@ -75,13 +76,12 @@ const routes = [
|
||||
systemRouter.routes(),
|
||||
toolboxRouter.routes(),
|
||||
trayRouter.routes(),
|
||||
uploadRouter.routes()
|
||||
uploadRouter.routes(),
|
||||
manageRouter.routes()
|
||||
]
|
||||
|
||||
for (const route of routes) {
|
||||
rpcServer.use(route)
|
||||
}
|
||||
|
||||
export {
|
||||
rpcServer
|
||||
}
|
||||
export { rpcServer }
|
||||
|
||||
@@ -20,7 +20,7 @@ export class RPCRouter implements IRPCRouter {
|
||||
return this
|
||||
}
|
||||
|
||||
routes () {
|
||||
routes() {
|
||||
return this.routeMap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { clipboard } from 'electron'
|
||||
|
||||
import { GalleryDB } from '@core/datastore'
|
||||
@@ -8,7 +7,12 @@ import { IFilter, IObject } from '@picgo/store/dist/types'
|
||||
import GuiApi from 'apis/gui'
|
||||
|
||||
import { RPCRouter } from '~/events/rpc/router'
|
||||
import { removeFileFromDogeInMain, removeFileFromHuaweiInMain, removeFileFromS3InMain, removeFileFromSFTPInMain } from '~/utils/deleteFunc'
|
||||
import {
|
||||
removeFileFromDogeInMain,
|
||||
removeFileFromHuaweiInMain,
|
||||
removeFileFromS3InMain,
|
||||
removeFileFromSFTPInMain
|
||||
} from '~/utils/deleteFunc'
|
||||
import pasteTemplate from '~/utils/pasteTemplate'
|
||||
|
||||
import { ICOREBuildInEvent, IPasteStyle, IRPCActionType, IRPCType } from '#/types/enum'
|
||||
@@ -19,7 +23,7 @@ const galleryRouter = new RPCRouter()
|
||||
const galleryRoutes = [
|
||||
{
|
||||
action: IRPCActionType.GALLERY_PASTE_TEXT,
|
||||
handler: async (_: IIPCEvent, args: [ item: ImgInfo, copy?: boolean]) => {
|
||||
handler: async (_: IIPCEvent, args: [item: ImgInfo, copy?: boolean]) => {
|
||||
const [item, copy = true] = args
|
||||
const pasteStyle = picgo.getConfig<IPasteStyle>(configPaths.settings.pasteStyle) || IPasteStyle.MARKDOWN
|
||||
const customLink = picgo.getConfig<string>(configPaths.settings.customLink)
|
||||
@@ -128,6 +132,4 @@ const galleryRoutes = [
|
||||
|
||||
galleryRouter.addBatch(galleryRoutes)
|
||||
|
||||
export {
|
||||
galleryRouter
|
||||
}
|
||||
export { galleryRouter }
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { ManageApi } from '~/manage/manageApi'
|
||||
|
||||
import { IRPCActionType, IRPCType } from '#/types/enum'
|
||||
|
||||
export default [
|
||||
{
|
||||
action: IRPCActionType.MANAGE_GET_BUCKET_LIST,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string]) => {
|
||||
return new ManageApi(args[0]).getBucketList()
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_GET_BUCKET_LIST_BACKSTAGE,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).getBucketListBackstage(args[1])
|
||||
}
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_GET_BUCKET_LIST_RECURSIVELY,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).getBucketListRecursively(args[1])
|
||||
}
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_CREATE_BUCKET,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).createBucket(args[1])
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_GET_BUCKET_FILE_LIST,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).getBucketFileList(args[1])
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_GET_BUCKET_DOMAIN,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).getBucketDomain(args[1])
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_SET_BUCKET_ACL_POLICY,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).setBucketAclPolicy(args[1])
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_RENAME_BUCKET_FILE,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).renameBucketFile(args[1])
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_DELETE_BUCKET_FILE,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).deleteBucketFile(args[1])
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_DELETE_BUCKET_FOLDER,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).deleteBucketFolder(args[1])
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_GET_PRE_SIGNED_URL,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).getPreSignedUrl(args[1])
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_UPLOAD_BUCKET_FILE,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).uploadBucketFile(args[1])
|
||||
}
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_DOWNLOAD_BUCKET_FILE,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).downloadBucketFile(args[1])
|
||||
}
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_CREATE_BUCKET_FOLDER,
|
||||
handler: async (_: IIPCEvent, args: [currentPicBed: string, param: IStringKeyMap]) => {
|
||||
return new ManageApi(args[0]).createBucketFolder(args[1])
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IRPCActionType, IRPCType } from '#/types/enum'
|
||||
import getManageApi from '~/manage/Main'
|
||||
|
||||
const manageApi = getManageApi()
|
||||
|
||||
export default [
|
||||
{
|
||||
action: IRPCActionType.MANAGE_GET_CONFIG,
|
||||
handler: async (_: IIPCEvent, args: [key?: string]) => {
|
||||
return manageApi.getConfig(args[0])
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_SAVE_CONFIG,
|
||||
handler: async (_: IIPCEvent, args: [data: IObj]) => {
|
||||
manageApi.saveConfig(args[0])
|
||||
}
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_REMOVE_CONFIG,
|
||||
handler: async (_: IIPCEvent, args: [key: string, propName: string]) => {
|
||||
manageApi.removeConfig(args[0], args[1])
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
import { RPCRouter } from '~/events/rpc/router'
|
||||
|
||||
import configRoutes from '~/events/rpc/routes/manage/config'
|
||||
import bucketRoutes from '~/events/rpc/routes/manage/bucket'
|
||||
import upDownLoadRoutes from '~/events/rpc/routes/manage/upDownload'
|
||||
|
||||
const manageRouter = new RPCRouter()
|
||||
|
||||
const manageRoutes = [...configRoutes, ...bucketRoutes, ...upDownLoadRoutes]
|
||||
|
||||
manageRouter.addBatch(manageRoutes)
|
||||
|
||||
export { manageRouter }
|
||||
@@ -0,0 +1,108 @@
|
||||
import { app, dialog, shell } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
|
||||
import { IRPCActionType, IRPCType } from '#/types/enum'
|
||||
|
||||
import UpDownTaskQueue from '~/manage/datastore/upDownTaskQueue'
|
||||
import { downloadFileFromUrl } from '~/manage/utils/common'
|
||||
|
||||
export default [
|
||||
{
|
||||
action: IRPCActionType.MANAGE_OPEN_FILE_SELECT_DIALOG,
|
||||
handler: async () => {
|
||||
const res = await dialog.showOpenDialog({
|
||||
properties: ['openFile', 'multiSelections']
|
||||
})
|
||||
return res.canceled ? [] : res.filePaths
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_GET_UPLOAD_TASK_LIST,
|
||||
handler: async () => {
|
||||
return UpDownTaskQueue.getInstance().getAllUploadTask()
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_GET_DOWNLOAD_TASK_LIST,
|
||||
handler: async () => {
|
||||
return UpDownTaskQueue.getInstance().getAllDownloadTask()
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_DELETE_UPLOADED_TASK,
|
||||
handler: async () => {
|
||||
UpDownTaskQueue.getInstance().removeUploadedTask()
|
||||
}
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_DELETE_ALL_UPLOADED_TASK,
|
||||
handler: async () => {
|
||||
UpDownTaskQueue.getInstance().clearUploadTaskQueue()
|
||||
}
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_DELETE_DOWNLOADED_TASK,
|
||||
handler: async () => {
|
||||
UpDownTaskQueue.getInstance().removeDownloadedTask()
|
||||
}
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_DELETE_ALL_DOWNLOADED_TASK,
|
||||
handler: async () => {
|
||||
UpDownTaskQueue.getInstance().clearDownloadTaskQueue()
|
||||
}
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_SELECT_DOWNLOAD_FOLDER,
|
||||
handler: async () => {
|
||||
const res = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory']
|
||||
})
|
||||
return res.filePaths[0]
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_GET_DEFAULT_DOWNLOAD_FOLDER,
|
||||
handler: async () => {
|
||||
return app.getPath('downloads')
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_OPEN_DOWNLOADED_FOLDER,
|
||||
handler: async (_: IIPCEvent, args: [path?: string]) => {
|
||||
const path = args[0]
|
||||
if (path) {
|
||||
shell.showItemInFolder(path)
|
||||
} else {
|
||||
shell.openPath(app.getPath('downloads'))
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_OPEN_LOCAL_FILE,
|
||||
handler: async (_: IIPCEvent, args: [fullPath: string]) => {
|
||||
const fullPath = args[0]
|
||||
fs.existsSync(fullPath) ? shell.showItemInFolder(fullPath) : shell.openPath(path.dirname(fullPath))
|
||||
}
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_DOWNLOAD_FILE_FROM_URL,
|
||||
handler: async (_: IIPCEvent, args: [urls: string[]]) => {
|
||||
return await downloadFileFromUrl(args[0])
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.MANAGE_CONVERT_PATH_TO_BASE64,
|
||||
handler: async (_: IIPCEvent, args: [filePath: string]) => {
|
||||
return fs.readFileSync(args[0], 'base64')
|
||||
},
|
||||
type: IRPCType.INVOKE
|
||||
}
|
||||
]
|
||||
@@ -94,6 +94,4 @@ const picbedRoutes = [
|
||||
|
||||
picbedRouter.addBatch(picbedRoutes)
|
||||
|
||||
export {
|
||||
picbedRouter
|
||||
}
|
||||
export { picbedRouter }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { RPCRouter } from '~/events/rpc/router'
|
||||
import {
|
||||
pluginImportLocalFunc,
|
||||
@@ -32,6 +31,4 @@ const pluginRoutes = [
|
||||
|
||||
pluginRouter.addBatch(pluginRoutes)
|
||||
|
||||
export {
|
||||
pluginRouter
|
||||
}
|
||||
export { pluginRouter }
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ICOREBuildInEvent, IPicGoHelperType, IWindowList } from '#/types/enum'
|
||||
const STORE_PATH = dbPathDir()
|
||||
|
||||
// eslint-disable-next-line
|
||||
const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require
|
||||
const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require
|
||||
|
||||
// get uploader or transformer config
|
||||
const getConfig = (name: string, type: IPicGoHelperType, ctx: PicGoCore) => {
|
||||
@@ -104,15 +104,17 @@ const getPluginList = (): IPicGoPlugin[] => {
|
||||
const handleNPMError = (): IDispose => {
|
||||
const handler = (msg: string) => {
|
||||
if (msg === 'NPM is not installed') {
|
||||
dialog.showMessageBox({
|
||||
title: T('TIPS_ERROR'),
|
||||
message: T('TIPS_INSTALL_NODE_AND_RELOAD_PICGO'),
|
||||
buttons: ['Yes']
|
||||
}).then((res) => {
|
||||
if (res.response === 0) {
|
||||
shell.openExternal('https://nodejs.org/')
|
||||
}
|
||||
})
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: T('TIPS_ERROR'),
|
||||
message: T('TIPS_INSTALL_NODE_AND_RELOAD_PICGO'),
|
||||
buttons: ['Yes']
|
||||
})
|
||||
.then(res => {
|
||||
if (res.response === 0) {
|
||||
shell.openExternal('https://nodejs.org/')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
picgo.once(ICOREBuildInEvent.FAILED, handler)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { IRPCActionType } from '#/types/enum'
|
||||
import server from '~/server'
|
||||
import webServer from '~/server/webServer'
|
||||
|
||||
@@ -16,19 +16,15 @@ export default [
|
||||
action: IRPCActionType.CONFIGURE_MIGRATE_FROM_PICGO,
|
||||
handler: async () => {
|
||||
const picGoConfigPath = STORE_PATH.replace('piclist', 'picgo')
|
||||
const files = [
|
||||
'data.json',
|
||||
'data.bak.json',
|
||||
'picgo.db',
|
||||
'picgo.bak.db'
|
||||
]
|
||||
const files = ['data.json', 'data.bak.json', 'picgo.db', 'picgo.bak.db']
|
||||
try {
|
||||
await Promise.all(files.map(async file => {
|
||||
const sourcePath = path.join(picGoConfigPath, file)
|
||||
const targetPath = path.join(STORE_PATH, file.replace('picgo', 'piclist'))
|
||||
await fs.copy(sourcePath, targetPath, { overwrite: true })
|
||||
}
|
||||
))
|
||||
await Promise.all(
|
||||
files.map(async file => {
|
||||
const sourcePath = path.join(picGoConfigPath, file)
|
||||
const targetPath = path.join(STORE_PATH, file.replace('picgo', 'piclist'))
|
||||
await fs.copy(sourcePath, targetPath, { overwrite: true })
|
||||
})
|
||||
)
|
||||
return true
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
|
||||
@@ -7,15 +7,8 @@ import shortKeyRoutes from '~/events/rpc/routes/setting/shortKey'
|
||||
|
||||
const settingRouter = new RPCRouter()
|
||||
|
||||
const settingRoutes = [
|
||||
...advancedRoutes,
|
||||
...configureRoutes,
|
||||
...mainAppRoutes,
|
||||
...shortKeyRoutes
|
||||
]
|
||||
const settingRoutes = [...advancedRoutes, ...configureRoutes, ...mainAppRoutes, ...shortKeyRoutes]
|
||||
|
||||
settingRouter.addBatch(settingRoutes)
|
||||
|
||||
export {
|
||||
settingRouter
|
||||
}
|
||||
export { settingRouter }
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
Notification
|
||||
} from 'electron'
|
||||
import { Notification } from 'electron'
|
||||
|
||||
import bus from '@core/bus'
|
||||
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
|
||||
|
||||
@@ -4,13 +4,8 @@ import windowRoutes from '~/events/rpc/routes/system/window'
|
||||
|
||||
const systemRouter = new RPCRouter()
|
||||
|
||||
const systemRoutes = [
|
||||
...appRoutes,
|
||||
...windowRoutes
|
||||
]
|
||||
const systemRoutes = [...appRoutes, ...windowRoutes]
|
||||
|
||||
systemRouter.addBatch(systemRoutes)
|
||||
|
||||
export {
|
||||
systemRouter
|
||||
}
|
||||
export { systemRouter }
|
||||
|
||||
@@ -2,12 +2,7 @@ import { app, BrowserWindow } from 'electron'
|
||||
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
|
||||
import {
|
||||
buildMainPageMenu,
|
||||
buildMiniPageMenu,
|
||||
buildPicBedListMenu,
|
||||
buildPluginPageMenu
|
||||
} from '~/events/remotes/menu'
|
||||
import { buildMainPageMenu, buildMiniPageMenu, buildPicBedListMenu, buildPluginPageMenu } from '~/events/remotes/menu'
|
||||
import { openMiniWindow } from '~/utils/windowHelper'
|
||||
|
||||
import { IRPCActionType, IWindowList } from '#/types/enum'
|
||||
|
||||
@@ -14,10 +14,8 @@ const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.HAS_PROBLEM_WITH_
|
||||
|
||||
const defaultClipboardImagePath = path.join(defaultConfigPath, CLIPBOARD_IMAGE_FOLDER)
|
||||
|
||||
export const checkClipboardUploadMap: IToolboxCheckerMap<
|
||||
IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD
|
||||
> = {
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: async (event) => {
|
||||
export const checkClipboardUploadMap: IToolboxCheckerMap<IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD> = {
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: async event => {
|
||||
sendToolboxRes(event, {
|
||||
status: IToolboxItemCheckStatus.LOADING
|
||||
})
|
||||
@@ -54,9 +52,7 @@ IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD
|
||||
}
|
||||
}
|
||||
|
||||
export const fixClipboardUploadMap: IToolboxFixMap<
|
||||
IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD
|
||||
> = {
|
||||
export const fixClipboardUploadMap: IToolboxFixMap<IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD> = {
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: async () => {
|
||||
const configFilePath = dbPathChecker()
|
||||
const dirPath = path.dirname(configFilePath)
|
||||
|
||||
@@ -11,7 +11,7 @@ import { T } from '~/i18n'
|
||||
import { IToolboxItemCheckStatus, IToolboxItemType } from '#/types/enum'
|
||||
|
||||
export const checkFileMap: IToolboxCheckerMap<
|
||||
IToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN
|
||||
IToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN
|
||||
> = {
|
||||
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: async (event: IpcMainEvent) => {
|
||||
const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.IS_CONFIG_FILE_BROKEN)
|
||||
@@ -38,7 +38,7 @@ IToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN
|
||||
})
|
||||
}
|
||||
},
|
||||
[IToolboxItemType.IS_GALLERY_FILE_BROKEN]: async (event) => {
|
||||
[IToolboxItemType.IS_GALLERY_FILE_BROKEN]: async event => {
|
||||
const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.IS_GALLERY_FILE_BROKEN)
|
||||
sendToolboxRes(event, {
|
||||
status: IToolboxItemCheckStatus.LOADING
|
||||
@@ -63,7 +63,7 @@ IToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN
|
||||
}
|
||||
|
||||
export const fixFileMap: IToolboxFixMap<
|
||||
IToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN
|
||||
IToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN
|
||||
> = {
|
||||
[IToolboxItemType.IS_CONFIG_FILE_BROKEN]: async () => {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import axios, { AxiosRequestConfig } from 'axios'
|
||||
import fs from 'fs-extra'
|
||||
import { IConfig } from 'piclist'
|
||||
@@ -11,7 +10,7 @@ import { T } from '~/i18n'
|
||||
|
||||
import { IToolboxItemCheckStatus, IToolboxItemType } from '#/types/enum'
|
||||
|
||||
function getProxy (proxyStr: string): AxiosRequestConfig['proxy'] | null {
|
||||
function getProxy(proxyStr: string): AxiosRequestConfig['proxy'] | null {
|
||||
if (proxyStr) {
|
||||
try {
|
||||
const proxyOptions = new URL(proxyStr)
|
||||
@@ -27,10 +26,8 @@ function getProxy (proxyStr: string): AxiosRequestConfig['proxy'] | null {
|
||||
|
||||
const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.HAS_PROBLEM_WITH_PROXY)
|
||||
|
||||
export const checkProxyMap: IToolboxCheckerMap<
|
||||
IToolboxItemType.HAS_PROBLEM_WITH_PROXY
|
||||
> = {
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_PROXY]: async (event) => {
|
||||
export const checkProxyMap: IToolboxCheckerMap<IToolboxItemType.HAS_PROBLEM_WITH_PROXY> = {
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_PROXY]: async event => {
|
||||
sendToolboxRes(event, {
|
||||
status: IToolboxItemCheckStatus.LOADING
|
||||
})
|
||||
@@ -38,9 +35,8 @@ IToolboxItemType.HAS_PROBLEM_WITH_PROXY
|
||||
if (fs.existsSync(configFilePath)) {
|
||||
let config: IConfig | undefined
|
||||
try {
|
||||
config = await fs.readJSON(configFilePath) as IConfig
|
||||
} catch (e) {
|
||||
}
|
||||
config = (await fs.readJSON(configFilePath)) as IConfig
|
||||
} catch (e) {}
|
||||
if (!config) {
|
||||
return sendToolboxRes(event, {
|
||||
status: IToolboxItemCheckStatus.SUCCESS,
|
||||
|
||||
@@ -30,7 +30,7 @@ toolboxRouter
|
||||
handler(event as IpcMainEvent)
|
||||
}
|
||||
} else {
|
||||
// do check all
|
||||
// do check all
|
||||
for (const key in toolboxCheckMap) {
|
||||
const handler = toolboxCheckMap[key as IToolboxItemType]
|
||||
if (handler) {
|
||||
@@ -42,7 +42,8 @@ toolboxRouter
|
||||
IRPCType.SEND
|
||||
)
|
||||
.add(
|
||||
IRPCActionType.TOOLBOX_CHECK_FIX, async (event, args) => {
|
||||
IRPCActionType.TOOLBOX_CHECK_FIX,
|
||||
async (event, args) => {
|
||||
const [type] = args as IToolboxCheckArgs
|
||||
const handler = toolboxFixMap[type]
|
||||
if (handler) {
|
||||
@@ -52,6 +53,4 @@ toolboxRouter
|
||||
IRPCType.INVOKE
|
||||
)
|
||||
|
||||
export {
|
||||
toolboxRouter
|
||||
}
|
||||
export { toolboxRouter }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IpcMainEvent } from 'electron'
|
||||
import { IRPCActionType, IToolboxItemType } from '#/types/enum'
|
||||
|
||||
export function sendToolboxResWithType (type: IToolboxItemType) {
|
||||
export function sendToolboxResWithType(type: IToolboxItemType) {
|
||||
return (event: IpcMainEvent, res?: Omit<IToolboxCheckRes, 'type'>) => {
|
||||
return event.sender.send(IRPCActionType.TOOLBOX_CHECK_RES, {
|
||||
...res,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
Notification
|
||||
} from 'electron'
|
||||
import { Notification } from 'electron'
|
||||
|
||||
import { RPCRouter } from '~/events/rpc/router'
|
||||
import { generateShortUrl, setTrayToolTip, handleCopyUrl } from '~/utils/common'
|
||||
@@ -42,8 +40,11 @@ const trayRoutes = [
|
||||
const img = await uploader.setWebContents(trayWindow.webContents).uploadWithBuildInClipboard()
|
||||
if (img !== false) {
|
||||
const pasteStyle = db.get(configPaths.settings.pasteStyle) || IPasteStyle.MARKDOWN
|
||||
handleCopyUrl(await (pasteTemplate(pasteStyle, img[0], db.get(configPaths.settings.customLink))))
|
||||
const isShowResultNotification = db.get(configPaths.settings.uploadResultNotification) === undefined ? true : !!db.get(configPaths.settings.uploadResultNotification)
|
||||
handleCopyUrl(await pasteTemplate(pasteStyle, img[0], db.get(configPaths.settings.customLink)))
|
||||
const isShowResultNotification =
|
||||
db.get(configPaths.settings.uploadResultNotification) === undefined
|
||||
? true
|
||||
: !!db.get(configPaths.settings.uploadResultNotification)
|
||||
if (isShowResultNotification) {
|
||||
const notification = new Notification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
@@ -66,6 +67,4 @@ const trayRoutes = [
|
||||
|
||||
trayRouter.addBatch(trayRoutes)
|
||||
|
||||
export {
|
||||
trayRouter
|
||||
}
|
||||
export { trayRouter }
|
||||
|
||||
@@ -30,6 +30,4 @@ const uploadRoutes = [
|
||||
|
||||
uploadRouter.addBatch(uploadRoutes)
|
||||
|
||||
export {
|
||||
uploadRouter
|
||||
}
|
||||
export { uploadRouter }
|
||||
|
||||
@@ -12,7 +12,7 @@ const serverPort = 36699
|
||||
|
||||
let server: http.Server
|
||||
|
||||
export function startFileServer () {
|
||||
export function startFileServer() {
|
||||
server = http.createServer((req, res) => {
|
||||
const requestPath = req.url?.split('?')[0]
|
||||
const filePath = path.join(imgFilePath, decodeURIComponent(requestPath as string))
|
||||
@@ -27,14 +27,16 @@ export function startFileServer () {
|
||||
})
|
||||
})
|
||||
|
||||
server.listen(serverPort, () => {
|
||||
logger.info(`File server is running, http://localhost:${serverPort}`)
|
||||
}).on('error', (err) => {
|
||||
logger.error(err)
|
||||
})
|
||||
server
|
||||
.listen(serverPort, () => {
|
||||
logger.info(`File server is running, http://localhost:${serverPort}`)
|
||||
})
|
||||
.on('error', err => {
|
||||
logger.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
export function stopFileServer () {
|
||||
export function stopFileServer() {
|
||||
server.close(() => {
|
||||
logger.info('File server is stopped')
|
||||
})
|
||||
|
||||
@@ -15,18 +15,18 @@ class I18nManager {
|
||||
readonly defaultLanguage: string = 'zh-CN'
|
||||
private i18nFileList: II18nItem[] = builtinI18nList
|
||||
|
||||
setOutterI18nFolder (folder: string) {
|
||||
setOutterI18nFolder(folder: string) {
|
||||
this.outterI18nFolder = folder
|
||||
}
|
||||
|
||||
addI18nFile (file: string, label: string) {
|
||||
addI18nFile(file: string, label: string) {
|
||||
this.i18nFileList.push({
|
||||
label,
|
||||
value: file
|
||||
})
|
||||
}
|
||||
|
||||
private getLocales (lang: string): ILocales {
|
||||
private getLocales(lang: string): ILocales {
|
||||
if (this.localesMap.has(lang)) {
|
||||
return this.localesMap.get(lang)!
|
||||
}
|
||||
@@ -53,13 +53,13 @@ class I18nManager {
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentLanguage (lang: string) {
|
||||
setCurrentLanguage(lang: string) {
|
||||
const locales = this.getLocales(lang)
|
||||
this.currentLanguage = lang
|
||||
this.initI18n(lang, locales)
|
||||
}
|
||||
|
||||
private initI18n (lang: string = this.defaultLanguage, locales: ILocales) {
|
||||
private initI18n(lang: string = this.defaultLanguage, locales: ILocales) {
|
||||
const objectAdapter = new ObjectAdapter({
|
||||
[lang]: locales
|
||||
})
|
||||
@@ -69,15 +69,15 @@ class I18nManager {
|
||||
})
|
||||
}
|
||||
|
||||
T (key: ILocalesKey, args: IStringKeyMap = {}): string {
|
||||
T(key: ILocalesKey, args: IStringKeyMap = {}): string {
|
||||
return this.i18n?.translate(key, args) || key
|
||||
}
|
||||
|
||||
get languageList () {
|
||||
get languageList() {
|
||||
return this.i18nFileList
|
||||
}
|
||||
|
||||
getCurrentLocales () {
|
||||
getCurrentLocales() {
|
||||
return {
|
||||
lang: this.currentLanguage,
|
||||
locales: this.getLocales(this.currentLanguage)
|
||||
|
||||
@@ -24,9 +24,9 @@ process.on('unhandledRejection', (error: any) => {
|
||||
})
|
||||
|
||||
// acconrding to https://github.com/Molunerfinn/PicGo/commit/7363be798cfef11e980934e542817ff1d6c04389#diff-896d0db4fbd446798fbffec14d456b4cd98d4c72c46856c770a585fa7ab0926f
|
||||
function bootstrapEPIPESuppression () {
|
||||
function bootstrapEPIPESuppression() {
|
||||
let suppressing = false
|
||||
function logEPIPEErrorOnce () {
|
||||
function logEPIPEErrorOnce() {
|
||||
if (suppressing) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
// 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
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck since the module is not pure ESM
|
||||
// shell-path 3.0.0 not work
|
||||
|
||||
const shellPath = require('shell-path')
|
||||
|
||||
export default function fixPath () {
|
||||
export default function fixPath() {
|
||||
if (process.platform === 'win32') {
|
||||
return
|
||||
}
|
||||
|
||||
process.env.PATH = shellPath.sync() || [
|
||||
'./node_modules/.bin',
|
||||
'/.nodebrew/current/bin',
|
||||
'/usr/local/bin',
|
||||
process.env.PATH
|
||||
].join(':')
|
||||
process.env.PATH =
|
||||
shellPath.sync() || ['./node_modules/.bin', '/.nodebrew/current/bin', '/usr/local/bin', process.env.PATH].join(':')
|
||||
}
|
||||
|
||||
+62
-69
@@ -1,19 +1,9 @@
|
||||
import axios from 'axios'
|
||||
import fs from 'fs-extra'
|
||||
import {
|
||||
app,
|
||||
globalShortcut,
|
||||
protocol,
|
||||
Notification,
|
||||
dialog,
|
||||
screen,
|
||||
shell
|
||||
} from 'electron'
|
||||
import { app, globalShortcut, protocol, Notification, dialog, screen, shell } from 'electron'
|
||||
import { UpdateInfo, autoUpdater } from 'electron-updater'
|
||||
import path from 'path'
|
||||
import {
|
||||
createProtocol
|
||||
} from 'vue-cli-plugin-electron-builder/lib'
|
||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
|
||||
|
||||
import bus from '@core/bus'
|
||||
import db from '@core/datastore'
|
||||
@@ -22,13 +12,8 @@ import logger from '@core/picgo/logger'
|
||||
|
||||
import { remoteNoticeHandler } from 'apis/app/remoteNotice'
|
||||
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
|
||||
import {
|
||||
createTray, setDockMenu
|
||||
} from 'apis/app/system'
|
||||
import {
|
||||
uploadChoosedFiles,
|
||||
uploadClipboardFiles
|
||||
} from 'apis/app/uploader/apis'
|
||||
import { createTray, setDockMenu } from 'apis/app/system'
|
||||
import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
|
||||
import busEventList from '~/events/busEventList'
|
||||
@@ -37,7 +22,6 @@ import { T } from '~/i18n'
|
||||
import '~/lifeCycle/errorHandler'
|
||||
import fixPath from '~/lifeCycle/fixPath'
|
||||
import UpDownTaskQueue from '~/manage/datastore/upDownTaskQueue'
|
||||
import { manageIpcList } from '~/manage/events/ipcList'
|
||||
import getManageApi from '~/manage/Main'
|
||||
import { clearTempFolder } from '~/manage/utils/common'
|
||||
import server from '~/server/index'
|
||||
@@ -57,7 +41,8 @@ const isDevelopment = process.env.NODE_ENV !== 'production'
|
||||
|
||||
const handleStartUpFiles = (argv: string[], cwd: string) => {
|
||||
const files = getUploadFiles(argv, cwd, logger)
|
||||
if (files === null || files.length > 0) { // 如果有文件列表作为参数,说明是命令行启动
|
||||
if (files === null || files.length > 0) {
|
||||
// 如果有文件列表作为参数,说明是命令行启动
|
||||
if (files === null) {
|
||||
logger.info('cli -> uploading file from clipboard')
|
||||
uploadClipboardFiles()
|
||||
@@ -83,34 +68,43 @@ autoUpdater.on('update-available', async (info: UpdateInfo) => {
|
||||
const lang = db.get(configPaths.settings.language) || II18nLanguage.ZH_CN
|
||||
let updateLog = ''
|
||||
try {
|
||||
const url = lang === II18nLanguage.ZH_CN ? 'https://release.piclist.cn/currentVersion.md' : 'https://release.piclist.cn/currentVersion_en.md'
|
||||
const url =
|
||||
lang === II18nLanguage.ZH_CN
|
||||
? 'https://release.piclist.cn/currentVersion.md'
|
||||
: 'https://release.piclist.cn/currentVersion_en.md'
|
||||
const res = await axios.get(url)
|
||||
updateLog = res.data
|
||||
} catch (e: any) {
|
||||
logger.error(e)
|
||||
}
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: T('FIND_NEW_VERSION'),
|
||||
buttons: ['Yes', 'Go to download page'],
|
||||
message: T('TIPS_FIND_NEW_VERSION', {
|
||||
v: info.version
|
||||
}) + '\n\n' + updateLog,
|
||||
checkboxLabel: T('NO_MORE_NOTICE'),
|
||||
checkboxChecked: false
|
||||
}).then((result) => {
|
||||
if (result.response === 0) {
|
||||
autoUpdater.downloadUpdate()
|
||||
} else {
|
||||
shell.openExternal('https://github.com/Kuingsmile/PicList/releases/latest')
|
||||
}
|
||||
db.set(configPaths.settings.showUpdateTip, !result.checkboxChecked)
|
||||
}).catch((err) => {
|
||||
logger.error(err)
|
||||
})
|
||||
dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: T('FIND_NEW_VERSION'),
|
||||
buttons: ['Yes', 'Go to download page'],
|
||||
message:
|
||||
T('TIPS_FIND_NEW_VERSION', {
|
||||
v: info.version
|
||||
}) +
|
||||
'\n\n' +
|
||||
updateLog,
|
||||
checkboxLabel: T('NO_MORE_NOTICE'),
|
||||
checkboxChecked: false
|
||||
})
|
||||
.then(result => {
|
||||
if (result.response === 0) {
|
||||
autoUpdater.downloadUpdate()
|
||||
} else {
|
||||
shell.openExternal('https://github.com/Kuingsmile/PicList/releases/latest')
|
||||
}
|
||||
db.set(configPaths.settings.showUpdateTip, !result.checkboxChecked)
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error(err)
|
||||
})
|
||||
})
|
||||
|
||||
autoUpdater.on('download-progress', (progressObj) => {
|
||||
autoUpdater.on('download-progress', progressObj => {
|
||||
const percent = {
|
||||
progress: progressObj.percent
|
||||
}
|
||||
@@ -119,41 +113,43 @@ autoUpdater.on('download-progress', (progressObj) => {
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: T('UPDATE_DOWNLOADED'),
|
||||
buttons: ['Yes', 'No'],
|
||||
message: T('TIPS_UPDATE_DOWNLOADED')
|
||||
}).then((result) => {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
window.webContents.send('updateProgress', { progress: 100 })
|
||||
if (result.response === 0) {
|
||||
autoUpdater.quitAndInstall()
|
||||
}
|
||||
}).catch((err) => {
|
||||
logger.error(err)
|
||||
})
|
||||
dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: T('UPDATE_DOWNLOADED'),
|
||||
buttons: ['Yes', 'No'],
|
||||
message: T('TIPS_UPDATE_DOWNLOADED')
|
||||
})
|
||||
.then(result => {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
window.webContents.send('updateProgress', { progress: 100 })
|
||||
if (result.response === 0) {
|
||||
autoUpdater.quitAndInstall()
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error(err)
|
||||
})
|
||||
})
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
autoUpdater.on('error', err => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
class LifeCycle {
|
||||
async #beforeReady () {
|
||||
async #beforeReady() {
|
||||
protocol.registerSchemesAsPrivileged([{ scheme: 'picgo', privileges: { secure: true, standard: true } }])
|
||||
// fix the $PATH in macOS & linux
|
||||
fixPath()
|
||||
beforeOpen()
|
||||
initI18n()
|
||||
rpcServer.start()
|
||||
getManageApi()
|
||||
UpDownTaskQueue.getInstance()
|
||||
manageIpcList.listen()
|
||||
initI18n()
|
||||
rpcServer.start()
|
||||
busEventList.listen()
|
||||
}
|
||||
|
||||
#onReady () {
|
||||
#onReady() {
|
||||
const readyFunction = async () => {
|
||||
createProtocol('picgo')
|
||||
windowManager.create(IWindowList.TRAY_WINDOW)
|
||||
@@ -173,7 +169,6 @@ class LifeCycle {
|
||||
const isHideDock = db.get(configPaths.settings.isHideDock) || false
|
||||
const startMode = db.get(configPaths.settings.startMode) || ISartMode.QUIET
|
||||
const currentPicBed = db.get(configPaths.picBed.uploader) || db.get(configPaths.picBed.current) || 'smms'
|
||||
// @ts-ignore
|
||||
const currentPicBedConfig = db.get(`picBed.${currentPicBed}`)?._configName || 'Default'
|
||||
const tooltip = `${currentPicBed} ${currentPicBedConfig}`
|
||||
if (process.platform === 'darwin') {
|
||||
@@ -237,7 +232,7 @@ class LifeCycle {
|
||||
app.whenReady().then(readyFunction)
|
||||
}
|
||||
|
||||
#onRunning () {
|
||||
#onRunning() {
|
||||
app.on('second-instance', (_, commandLine, workingDirectory) => {
|
||||
logger.info('detect second instance')
|
||||
const result = handleStartUpFiles(commandLine, workingDirectory)
|
||||
@@ -272,7 +267,7 @@ class LifeCycle {
|
||||
}
|
||||
}
|
||||
|
||||
#onQuit () {
|
||||
#onQuit() {
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
@@ -304,7 +299,7 @@ class LifeCycle {
|
||||
}
|
||||
}
|
||||
|
||||
async launchApp () {
|
||||
async launchApp() {
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
if (!gotTheLock) {
|
||||
app.quit()
|
||||
@@ -319,6 +314,4 @@ class LifeCycle {
|
||||
|
||||
const bootstrap = new LifeCycle()
|
||||
|
||||
export {
|
||||
bootstrap
|
||||
}
|
||||
export { bootstrap }
|
||||
|
||||
+204
-149
@@ -8,7 +8,13 @@ import windowManager from 'apis/app/window/windowManager'
|
||||
|
||||
import UpDownTaskQueue from '~/manage/datastore/upDownTaskQueue'
|
||||
import { ManageLogger } from '~/manage/utils/logger'
|
||||
import { hmacSha1Base64, getFileMimeType, formatError, NewDownloader, ConcurrencyPromisePool } from '~/manage/utils/common'
|
||||
import {
|
||||
hmacSha1Base64,
|
||||
getFileMimeType,
|
||||
formatError,
|
||||
NewDownloader,
|
||||
ConcurrencyPromisePool
|
||||
} from '~/manage/utils/common'
|
||||
|
||||
import { commonTaskStatus, IWindowList, uploadTaskSpecialStatus } from '#/types/enum'
|
||||
import { isImage } from '#/utils/common'
|
||||
@@ -22,7 +28,7 @@ class AliyunApi {
|
||||
timeOut = 30000
|
||||
logger: ManageLogger
|
||||
|
||||
constructor (accessKeyId: string, accessKeySecret: string, logger: ManageLogger) {
|
||||
constructor(accessKeyId: string, accessKeySecret: string, logger: ManageLogger) {
|
||||
this.ctx = new OSS({
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
@@ -33,7 +39,7 @@ class AliyunApi {
|
||||
this.logger = logger
|
||||
}
|
||||
|
||||
formatFolder (item: string, slicedPrefix: string, urlPrefix: string): any {
|
||||
formatFolder(item: string, slicedPrefix: string, urlPrefix: string): any {
|
||||
return {
|
||||
key: item,
|
||||
url: `${urlPrefix}/${item}`,
|
||||
@@ -48,7 +54,7 @@ class AliyunApi {
|
||||
}
|
||||
}
|
||||
|
||||
formatFile (item: OSS.ObjectMeta, slicedPrefix: string, urlPrefix: string): any {
|
||||
formatFile(item: OSS.ObjectMeta, slicedPrefix: string, urlPrefix: string): any {
|
||||
const fileName = item.name.replace(slicedPrefix, '')
|
||||
return {
|
||||
...item,
|
||||
@@ -65,26 +71,32 @@ class AliyunApi {
|
||||
}
|
||||
}
|
||||
|
||||
getCanonicalizedOSSHeaders (headers: IStringKeyMap) {
|
||||
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) => {
|
||||
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) {
|
||||
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) {
|
||||
getNewCtx(region: string, bucket: string) {
|
||||
return new OSS({
|
||||
accessKeyId: this.accessKeyId,
|
||||
accessKeySecret: this.accessKeySecret,
|
||||
@@ -95,21 +107,25 @@ class AliyunApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储桶列表
|
||||
*/
|
||||
async getBucketList (): Promise<any> {
|
||||
* 获取存储桶列表
|
||||
*/
|
||||
async getBucketList(): Promise<any> {
|
||||
const getBuckets = async (marker?: string) => {
|
||||
const res = await this.ctx.listBuckets({
|
||||
const res = (await this.ctx.listBuckets({
|
||||
marker,
|
||||
'max-keys': 1000
|
||||
}) as IStringKeyMap
|
||||
})) as IStringKeyMap
|
||||
if (res?.res?.statusCode !== 200 || !res?.buckets) return { result: [], isTruncated: false }
|
||||
const formattedBuckets = res.buckets.map((item: OSS.Bucket) => ({
|
||||
Name: item.name,
|
||||
Location: item.region,
|
||||
CreationDate: item.creationDate
|
||||
}))
|
||||
return { result: formattedBuckets, isTruncated: res.isTruncated, nextMarker: res.nextMarker }
|
||||
return {
|
||||
result: formattedBuckets,
|
||||
isTruncated: res.isTruncated,
|
||||
nextMarker: res.nextMarker
|
||||
}
|
||||
}
|
||||
const result: IStringKeyMap[] = []
|
||||
let NextMarker: string | undefined
|
||||
@@ -127,7 +143,7 @@ class AliyunApi {
|
||||
/**
|
||||
* 获取自定义域名
|
||||
*/
|
||||
async getBucketDomain (param: IStringKeyMap): Promise<any> {
|
||||
async getBucketDomain(param: IStringKeyMap): Promise<any> {
|
||||
const headers = {
|
||||
Date: new Date().toUTCString()
|
||||
}
|
||||
@@ -160,17 +176,17 @@ class AliyunApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建存储桶
|
||||
* @param {Object} configMap
|
||||
* configMap = {
|
||||
* BucketName: string,
|
||||
* region: string,
|
||||
* acl: string
|
||||
* }
|
||||
* @description
|
||||
* acl: private | publicRead | publicReadWrite
|
||||
*/
|
||||
async createBucket (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 创建存储桶
|
||||
* @param {Object} configMap
|
||||
* configMap = {
|
||||
* BucketName: string,
|
||||
* region: string,
|
||||
* acl: string
|
||||
* }
|
||||
* @description
|
||||
* acl: private | publicRead | publicReadWrite
|
||||
*/
|
||||
async createBucket(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const client = new OSS({
|
||||
accessKeyId: this.accessKeyId,
|
||||
accessKeySecret: this.accessKeySecret,
|
||||
@@ -191,9 +207,14 @@ class AliyunApi {
|
||||
return res?.res?.status === 200
|
||||
}
|
||||
|
||||
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListRecursively(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, cancelToken } = configMap
|
||||
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
|
||||
@@ -212,13 +233,16 @@ class AliyunApi {
|
||||
}
|
||||
const client = this.getNewCtx(region, bucket)
|
||||
do {
|
||||
res = await client.listV2({
|
||||
prefix: slicedPrefix === '' ? undefined : slicedPrefix,
|
||||
'max-keys': '1000',
|
||||
'continuation-token': marker
|
||||
}, {
|
||||
timeout: this.timeOut
|
||||
})
|
||||
res = await client.listV2(
|
||||
{
|
||||
prefix: slicedPrefix === '' ? undefined : slicedPrefix,
|
||||
'max-keys': '1000',
|
||||
'continuation-token': marker
|
||||
},
|
||||
{
|
||||
timeout: this.timeOut
|
||||
}
|
||||
)
|
||||
if (res?.res?.statusCode === 200) {
|
||||
res?.objects?.forEach((item: OSS.ObjectMeta) => {
|
||||
item.size !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
|
||||
@@ -238,9 +262,14 @@ class AliyunApi {
|
||||
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
|
||||
}
|
||||
|
||||
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, cancelToken } = configMap
|
||||
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
|
||||
@@ -259,14 +288,17 @@ class AliyunApi {
|
||||
}
|
||||
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
|
||||
})
|
||||
res = await client.listV2(
|
||||
{
|
||||
prefix: slicedPrefix === '' ? undefined : slicedPrefix,
|
||||
delimiter: '/',
|
||||
'max-keys': '1000',
|
||||
'continuation-token': marker
|
||||
},
|
||||
{
|
||||
timeout: this.timeOut
|
||||
}
|
||||
)
|
||||
if (res?.res?.statusCode === 200) {
|
||||
res?.prefixes?.forEach((item: string) => {
|
||||
result.fullList.push(this.formatFolder(item, slicedPrefix, urlPrefix))
|
||||
@@ -303,21 +335,30 @@ class AliyunApi {
|
||||
* itemsPerPage: number,
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getBucketFileList (configMap: IStringKeyMap): Promise<any> {
|
||||
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, marker, itemsPerPage } = configMap
|
||||
*/
|
||||
async getBucketFileList(configMap: IStringKeyMap): Promise<any> {
|
||||
const {
|
||||
bucketName: bucket,
|
||||
bucketConfig: { Location: region },
|
||||
prefix,
|
||||
marker,
|
||||
itemsPerPage
|
||||
} = configMap
|
||||
const slicedPrefix = prefix.slice(1)
|
||||
const urlPrefix = configMap.customUrl || `https://${bucket}.${region}.aliyuncs.com`
|
||||
|
||||
const client = this.getNewCtx(region, bucket)
|
||||
const res = await client.listV2({
|
||||
prefix: slicedPrefix || undefined,
|
||||
delimiter: '/',
|
||||
'max-keys': itemsPerPage.toString(),
|
||||
'continuation-token': marker
|
||||
}, {
|
||||
timeout: this.timeOut
|
||||
}) as any
|
||||
const res = (await client.listV2(
|
||||
{
|
||||
prefix: slicedPrefix || undefined,
|
||||
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.statusCode !== 200) {
|
||||
@@ -330,7 +371,9 @@ class AliyunApi {
|
||||
}
|
||||
const fullList = [
|
||||
...(res.prefixes?.map((item: string) => this.formatFolder(item, slicedPrefix, urlPrefix)) || []),
|
||||
...(res.objects?.filter((item: OSS.ObjectMeta) => item.size !== 0).map((item: OSS.ObjectMeta) => this.formatFile(item, slicedPrefix, urlPrefix)) || [])
|
||||
...(res.objects
|
||||
?.filter((item: OSS.ObjectMeta) => item.size !== 0)
|
||||
.map((item: OSS.ObjectMeta) => this.formatFile(item, slicedPrefix, urlPrefix)) || [])
|
||||
]
|
||||
return {
|
||||
fullList,
|
||||
@@ -341,50 +384,47 @@ class AliyunApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* oldKey: string,
|
||||
* newKey: string
|
||||
* }
|
||||
*/
|
||||
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 重命名文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* oldKey: string,
|
||||
* newKey: string
|
||||
* }
|
||||
*/
|
||||
async renameBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, oldKey, newKey } = configMap
|
||||
const client = this.getNewCtx(region, bucketName)
|
||||
const copyRes = await client.copy(
|
||||
newKey,
|
||||
oldKey
|
||||
) as any
|
||||
const copyRes = (await client.copy(newKey, oldKey)) as any
|
||||
if (copyRes?.res.statusCode === 200) {
|
||||
const deleteRes = await client.delete(oldKey) as any
|
||||
const deleteRes = (await client.delete(oldKey)) as any
|
||||
return deleteRes?.res.statusCode === 204
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, key } = configMap
|
||||
const client = this.getNewCtx(region, bucketName)
|
||||
const res = await client.delete(key) as any
|
||||
const res = (await client.delete(key)) as any
|
||||
return res?.res.statusCode === 204
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 删除文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async deleteBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, key } = configMap
|
||||
const client = this.getNewCtx(region, bucketName)
|
||||
let marker
|
||||
@@ -394,14 +434,17 @@ class AliyunApi {
|
||||
Contents: [] as any[]
|
||||
}
|
||||
do {
|
||||
const res = await client.listV2({
|
||||
prefix: key,
|
||||
delimiter: '/',
|
||||
'max-keys': '1000',
|
||||
'continuation-token': marker
|
||||
}, {
|
||||
timeout: this.timeOut
|
||||
}) as any
|
||||
const res = (await client.listV2(
|
||||
{
|
||||
prefix: key,
|
||||
delimiter: '/',
|
||||
'max-keys': '1000',
|
||||
'continuation-token': marker
|
||||
},
|
||||
{
|
||||
timeout: this.timeOut
|
||||
}
|
||||
)) as any
|
||||
if (res?.res.statusCode !== 200) return false
|
||||
|
||||
res.prefixes !== null && allFileList.CommonPrefixes.push(...res.prefixes)
|
||||
@@ -423,8 +466,9 @@ class AliyunApi {
|
||||
if (allFileList.Contents.length > 0) {
|
||||
const cycle = Math.ceil(allFileList.Contents.length / 1000)
|
||||
for (let i = 0; i < cycle; i++) {
|
||||
const deleteRes = await client.deleteMulti(
|
||||
allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => item.name)) as any
|
||||
const deleteRes = (await client.deleteMulti(
|
||||
allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => item.name)
|
||||
)) as any
|
||||
if (deleteRes?.res.statusCode !== 200) return false
|
||||
}
|
||||
}
|
||||
@@ -432,17 +476,17 @@ class AliyunApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预签名url
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string,
|
||||
* expires: number,
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
|
||||
* 获取预签名url
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string,
|
||||
* expires: number,
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getPreSignedUrl(configMap: IStringKeyMap): Promise<string> {
|
||||
const { bucketName, region, key, expires, customUrl } = configMap
|
||||
const client = this.getNewCtx(region, bucketName)
|
||||
const res = client.signatureUrl(key, {
|
||||
@@ -455,7 +499,7 @@ class AliyunApi {
|
||||
* 上传文件
|
||||
* @param configMap
|
||||
*/
|
||||
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { fileArray } = configMap
|
||||
// fileArray = [{
|
||||
// bucketName: string,
|
||||
@@ -485,10 +529,8 @@ class AliyunApi {
|
||||
targetFileBucket: bucketName,
|
||||
targetFileRegion: region
|
||||
})
|
||||
client.multipartUpload(
|
||||
key,
|
||||
filePath,
|
||||
{
|
||||
client
|
||||
.multipartUpload(key, filePath, {
|
||||
partSize: 1 * 1024 * 1024,
|
||||
mime: getFileMimeType(fileName),
|
||||
progress: (p: number) => {
|
||||
@@ -499,37 +541,43 @@ class AliyunApi {
|
||||
status: uploadTaskSpecialStatus.uploading
|
||||
})
|
||||
}
|
||||
}
|
||||
).then((res: any) => {
|
||||
const id = `${bucketName}-${region}-${key}-${filePath}`
|
||||
if (res?.res?.statusCode === 200) {
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 100,
|
||||
status: uploadTaskSpecialStatus.uploaded,
|
||||
response: JSON.stringify(res),
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
} else {
|
||||
})
|
||||
.then((res: any) => {
|
||||
const id = `${bucketName}-${region}-${key}-${filePath}`
|
||||
if (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(res),
|
||||
response: JSON.stringify(err),
|
||||
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
|
||||
}
|
||||
@@ -538,10 +586,10 @@ class AliyunApi {
|
||||
* 新建文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async createBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, key } = configMap
|
||||
const client = this.getNewCtx(region, bucketName)
|
||||
const res = await client.put(key, Buffer.from('')) as any
|
||||
const res = (await client.put(key, Buffer.from(''))) as any
|
||||
return res?.res?.statusCode === 200
|
||||
}
|
||||
|
||||
@@ -549,7 +597,7 @@ class AliyunApi {
|
||||
* 下载文件
|
||||
* @param configMap
|
||||
*/
|
||||
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { downloadPath, fileArray, maxDownloadFileCount } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
const promises = [] as any
|
||||
@@ -571,20 +619,27 @@ class AliyunApi {
|
||||
const preSignedUrl = client.signatureUrl(key, {
|
||||
expires: 60 * 60 * 48
|
||||
})
|
||||
promises.push(() => new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger)
|
||||
.then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
promises.push(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger).then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
}
|
||||
const pool = new ConcurrencyPromisePool(maxDownloadFileCount)
|
||||
pool.all(promises).catch((error: any) => {
|
||||
this.logger.error(formatError(error, { class: 'AliyunApi', method: 'downloadBucketFile' }))
|
||||
this.logger.error(
|
||||
formatError(error, {
|
||||
class: 'AliyunApi',
|
||||
method: 'downloadBucketFile'
|
||||
})
|
||||
)
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
+186
-104
@@ -6,7 +6,14 @@ import path from 'path'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
|
||||
import UpDownTaskQueue from '~/manage/datastore/upDownTaskQueue'
|
||||
import { gotUpload, NewDownloader, getAgent, getOptions, ConcurrencyPromisePool, formatError } from '~/manage/utils/common'
|
||||
import {
|
||||
gotUpload,
|
||||
NewDownloader,
|
||||
getAgent,
|
||||
getOptions,
|
||||
ConcurrencyPromisePool,
|
||||
formatError
|
||||
} from '~/manage/utils/common'
|
||||
import { ManageLogger } from '~/manage/utils/logger'
|
||||
|
||||
import { commonTaskStatus, IWindowList } from '#/types/enum'
|
||||
@@ -20,9 +27,9 @@ class GithubApi {
|
||||
proxy: any
|
||||
proxyStr: string | undefined
|
||||
baseUrl = 'https://api.github.com'
|
||||
commonHeaders : IStringKeyMap
|
||||
commonHeaders: IStringKeyMap
|
||||
|
||||
constructor (token: string, username: string, proxy: string | undefined, logger: ManageLogger) {
|
||||
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
|
||||
@@ -34,16 +41,25 @@ class GithubApi {
|
||||
}
|
||||
}
|
||||
|
||||
formatFolder (item: any, slicedPrefix: string, branch: string, repo: string, cdnUrl: string | undefined) {
|
||||
formatFolder(item: any, slicedPrefix: string, branch: string, repo: string, cdnUrl: string | undefined) {
|
||||
const key = `${slicedPrefix ? `${slicedPrefix}/` : ''}${item.path}/`
|
||||
let rawUrl = ''
|
||||
const placeholders = ['{username}', '{repo}', '{branch}', '{path}']
|
||||
rawUrl = cdnUrl
|
||||
? placeholders.some(item => cdnUrl.includes(item))
|
||||
? placeholders.reduce((url, ph) => {
|
||||
const value = ph === '{username}' ? this.username : ph === '{repo}' ? repo : ph === '{branch}' ? branch : ph === '{path}' ? key : ''
|
||||
return url.replaceAll(ph, value)
|
||||
}, cdnUrl)
|
||||
const value =
|
||||
ph === '{username}'
|
||||
? this.username
|
||||
: ph === '{repo}'
|
||||
? repo
|
||||
: ph === '{branch}'
|
||||
? branch
|
||||
: ph === '{path}'
|
||||
? key
|
||||
: ''
|
||||
return url.replaceAll(ph, value)
|
||||
}, cdnUrl)
|
||||
: `${cdnUrl}/${key}`
|
||||
: `https://raw.githubusercontent.com/${this.username}/${repo}/${branch}/${key}`
|
||||
rawUrl = rawUrl.replace(/(?<!https?:)\/{2,}/g, '/')
|
||||
@@ -62,16 +78,25 @@ class GithubApi {
|
||||
}
|
||||
}
|
||||
|
||||
formatFile (item: any, slicedPrefix: string, branch: string, repo: string, cdnUrl: string | undefined) {
|
||||
formatFile(item: any, slicedPrefix: string, branch: string, repo: string, cdnUrl: string | undefined) {
|
||||
let rawUrl = ''
|
||||
const placeholders = ['{username}', '{repo}', '{branch}', '{path}']
|
||||
const key = slicedPrefix === '' ? item.path : `${slicedPrefix}/${item.path}`
|
||||
rawUrl = cdnUrl
|
||||
? placeholders.some(item => cdnUrl.includes(item))
|
||||
? placeholders.reduce((url, ph) => {
|
||||
const value = ph === '{username}' ? this.username : ph === '{repo}' ? repo : ph === '{branch}' ? branch : ph === '{path}' ? `${slicedPrefix}/${item.path}` : ''
|
||||
return url.replaceAll(ph, value)
|
||||
}, cdnUrl)
|
||||
const value =
|
||||
ph === '{username}'
|
||||
? this.username
|
||||
: ph === '{repo}'
|
||||
? repo
|
||||
: ph === '{branch}'
|
||||
? branch
|
||||
: ph === '{path}'
|
||||
? `${slicedPrefix}/${item.path}`
|
||||
: ''
|
||||
return url.replaceAll(ph, value)
|
||||
}, cdnUrl)
|
||||
: `${cdnUrl}/${key}`
|
||||
: `https://raw.githubusercontent.com/${this.username}/${repo}/${branch}/${key}`
|
||||
rawUrl = rawUrl.replace(/(?<!https?:)\/{2,}/g, '/')
|
||||
@@ -92,17 +117,25 @@ class GithubApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* get repo list
|
||||
*/
|
||||
async getBucketList (): Promise<any> {
|
||||
* get repo list
|
||||
*/
|
||||
async getBucketList(): Promise<any> {
|
||||
let initPage = 1
|
||||
let res
|
||||
const result = [] as any[]
|
||||
do {
|
||||
res = await got(
|
||||
res = (await got(
|
||||
`${this.baseUrl}/user/repos`,
|
||||
getOptions('GET', this.commonHeaders, { page: initPage, per_page: 100 }, 'json', undefined, undefined, this.proxy)
|
||||
) as any
|
||||
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({
|
||||
@@ -123,16 +156,24 @@ class GithubApi {
|
||||
/**
|
||||
* 获取branch列表
|
||||
*/
|
||||
async getBucketDomain (param: IStringKeyMap): Promise<any> {
|
||||
async getBucketDomain(param: IStringKeyMap): Promise<any> {
|
||||
const { bucketName: repo } = param
|
||||
let initPage = 1
|
||||
let res
|
||||
const result = [] as string[]
|
||||
do {
|
||||
res = await got(
|
||||
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
|
||||
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 {
|
||||
@@ -143,7 +184,7 @@ class GithubApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListRecursively(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: repo, customUrl: branch, prefix, cancelToken, cdnUrl } = configMap
|
||||
const slicedPrefix = prefix.replace(/(^\/+|\/+$)/g, '')
|
||||
@@ -167,10 +208,10 @@ class GithubApi {
|
||||
return result
|
||||
}
|
||||
const currentPrefix = treeQueue[0]
|
||||
res = await got(
|
||||
res = (await got(
|
||||
`${this.baseUrl}/repos/${this.username}/${repo}/git/trees/${branch}:${treeQueue.shift()}`,
|
||||
getOptions('GET', this.commonHeaders, {}, 'json', undefined, undefined, this.proxy)
|
||||
) as any
|
||||
)) as any
|
||||
if (res && res.statusCode === 200) {
|
||||
const { tree } = res.body
|
||||
tree.forEach((item: any) => {
|
||||
@@ -194,7 +235,7 @@ class GithubApi {
|
||||
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
|
||||
}
|
||||
|
||||
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: repo, customUrl: branch, prefix, cancelToken, cdnUrl } = configMap
|
||||
const slicedPrefix = prefix.replace(/(^\/+|\/+$)/g, '')
|
||||
@@ -236,15 +277,15 @@ class GithubApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName: repo, githubBranch: branch, key, DeleteHash: sha } = configMap
|
||||
const body = {
|
||||
message: 'deleted by PicList',
|
||||
@@ -259,94 +300,135 @@ class GithubApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* create a new tree to delete a folder
|
||||
* @param configMap
|
||||
*/
|
||||
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* create a new tree to delete a folder
|
||||
* @param configMap
|
||||
*/
|
||||
async deleteBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName: repo, githubBranch: branch, key } = configMap
|
||||
// get sha of the branch
|
||||
const refRes = await got(
|
||||
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
|
||||
)) as any
|
||||
if (refRes.statusCode !== 200) return false
|
||||
const refSha = refRes.body.object.sha
|
||||
// get sha of the root tree
|
||||
const rootRes = await got(
|
||||
const rootRes = (await got(
|
||||
`${this.baseUrl}/repos/${this.username}/${repo}/branches/${branch}`,
|
||||
getOptions('GET', undefined, undefined, 'json', undefined, undefined, this.proxy)
|
||||
) as any
|
||||
)) 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
|
||||
// get sha of the folder tree
|
||||
const treeRes = await got(
|
||||
const treeRes = (await got(
|
||||
`${this.baseUrl}/repos/${this.username}/${repo}/git/trees/${branch}:${key.replace(/(^\/+|\/+$)/g, '')}`,
|
||||
getOptions('GET', this.commonHeaders, {
|
||||
recursive: true
|
||||
}, 'json', undefined, undefined, this.proxy)
|
||||
) as any
|
||||
getOptions(
|
||||
'GET',
|
||||
this.commonHeaders,
|
||||
{
|
||||
recursive: true
|
||||
},
|
||||
'json',
|
||||
undefined,
|
||||
undefined,
|
||||
this.proxy
|
||||
)
|
||||
)) as any
|
||||
if (treeRes.statusCode !== 200) return false
|
||||
const oldTree = treeRes.body.tree
|
||||
// create a new tree
|
||||
const newTree = oldTree.filter((item: any) => item.type === 'blob')
|
||||
.map((item:any) => ({
|
||||
const newTree = oldTree
|
||||
.filter((item: any) => item.type === 'blob')
|
||||
.map((item: any) => ({
|
||||
path: `${key.replace(/(^\/+|\/+$)/g, '')}/${item.path}`,
|
||||
mode: item.mode,
|
||||
type: item.type,
|
||||
sha: null
|
||||
}))
|
||||
const newTreeShaRes = await got(
|
||||
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
|
||||
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
|
||||
// create a new commit
|
||||
const commitRes = await got(
|
||||
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
|
||||
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
|
||||
// update the branch
|
||||
const updateRefRes = await got(
|
||||
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
|
||||
getOptions(
|
||||
'PATCH',
|
||||
this.commonHeaders,
|
||||
undefined,
|
||||
'json',
|
||||
JSON.stringify({
|
||||
sha: commitSha
|
||||
}),
|
||||
undefined,
|
||||
this.proxy
|
||||
)
|
||||
)) as any
|
||||
return updateRefRes.statusCode === 200
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预签名url
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string,
|
||||
* expires: number,
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
|
||||
* 获取预签名url
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string,
|
||||
* expires: number,
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getPreSignedUrl(configMap: IStringKeyMap): Promise<string> {
|
||||
const { bucketName: repo, customUrl: branch, key, rawUrl, githubPrivate: isPrivate } = configMap
|
||||
if (!isPrivate) return rawUrl
|
||||
const res = await got(
|
||||
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
|
||||
getOptions(
|
||||
'GET',
|
||||
this.commonHeaders,
|
||||
{
|
||||
ref: branch
|
||||
},
|
||||
'json',
|
||||
undefined,
|
||||
undefined,
|
||||
this.proxy
|
||||
)
|
||||
)) as any
|
||||
return res.statusCode === 200 ? res.body.download_url : ''
|
||||
}
|
||||
|
||||
@@ -354,7 +436,7 @@ class GithubApi {
|
||||
* 新建文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async createBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName: repo, githubBranch: branch, key } = configMap
|
||||
const newFileKey = `${trimPath(key)}/.gitkeep`
|
||||
const base64Content = Buffer.from('created by PicList').toString('base64')
|
||||
@@ -374,7 +456,7 @@ class GithubApi {
|
||||
* 上传文件
|
||||
* @param configMap
|
||||
*/
|
||||
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { fileArray } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
fileArray.forEach((item: any) => {
|
||||
@@ -423,7 +505,7 @@ class GithubApi {
|
||||
* 下载文件
|
||||
* @param configMap
|
||||
*/
|
||||
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { downloadPath, fileArray, maxDownloadFileCount } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
const promises = [] as any
|
||||
@@ -454,27 +536,27 @@ class GithubApi {
|
||||
} else {
|
||||
downloadUrl = githubUrl
|
||||
}
|
||||
promises.push(() => new Promise((resolve, reject) => {
|
||||
NewDownloader(
|
||||
instance,
|
||||
downloadUrl,
|
||||
id,
|
||||
savedFilePath,
|
||||
this.logger,
|
||||
this.proxyStr
|
||||
)
|
||||
.then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
promises.push(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, downloadUrl, id, savedFilePath, this.logger, this.proxyStr).then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
}
|
||||
const pool = new ConcurrencyPromisePool(maxDownloadFileCount)
|
||||
pool.all(promises).catch((error) => {
|
||||
this.logger.error(formatError(error, { class: 'GithubApi', method: 'downloadBucketFile' }))
|
||||
pool.all(promises).catch(error => {
|
||||
this.logger.error(
|
||||
formatError(error, {
|
||||
class: 'GithubApi',
|
||||
method: 'downloadBucketFile'
|
||||
})
|
||||
)
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class ImgurApi {
|
||||
idHeaders: any
|
||||
baseUrl = 'https://api.imgur.com/3'
|
||||
|
||||
constructor (userName: string, accessToken: string, proxy: any, logger: ManageLogger) {
|
||||
constructor(userName: string, accessToken: string, proxy: any, logger: ManageLogger) {
|
||||
this.userName = userName
|
||||
this.accessToken = accessToken.startsWith('Bearer ') ? accessToken : `Bearer ${accessToken}`
|
||||
this.proxy = proxy
|
||||
@@ -42,7 +42,7 @@ class ImgurApi {
|
||||
}
|
||||
}
|
||||
|
||||
formatFile (item: any) {
|
||||
formatFile(item: any) {
|
||||
const fileName = path.basename(item.link)
|
||||
const isImg = isImage(fileName)
|
||||
return {
|
||||
@@ -62,17 +62,17 @@ class ImgurApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* get repo list
|
||||
*/
|
||||
async getBucketList (): Promise<any> {
|
||||
* get repo list
|
||||
*/
|
||||
async getBucketList(): Promise<any> {
|
||||
let initPage = 0
|
||||
let res
|
||||
const result = [] as any[]
|
||||
do {
|
||||
res = await got(
|
||||
res = (await got(
|
||||
`${this.baseUrl}/account/${this.userName}/albums/${initPage}`,
|
||||
getOptions('GET', this.tokenHeaders, undefined, 'json', undefined, undefined, this.proxy)
|
||||
) as any
|
||||
)) as any
|
||||
if (!(res.statusCode === 200 && res.body.success)) {
|
||||
return []
|
||||
}
|
||||
@@ -93,9 +93,12 @@ class ImgurApi {
|
||||
return finalResult
|
||||
}
|
||||
|
||||
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketConfig: { Location: albumHash }, cancelToken } = configMap
|
||||
const {
|
||||
bucketConfig: { Location: albumHash },
|
||||
cancelToken
|
||||
} = configMap
|
||||
const cancelTask = [false]
|
||||
ipcMain.on('cancelLoadingFileList', (_: IpcMainEvent, token: string) => {
|
||||
if (token === cancelToken) {
|
||||
@@ -110,10 +113,10 @@ class ImgurApi {
|
||||
finished: false
|
||||
}
|
||||
if (albumHash !== 'unclassified') {
|
||||
res = await got(
|
||||
res = (await got(
|
||||
`${this.baseUrl}/account/${this.userName}/album/${albumHash}`,
|
||||
getOptions('GET', this.tokenHeaders, undefined, 'json', undefined, undefined, this.proxy)
|
||||
) as any
|
||||
)) as any
|
||||
if (res.statusCode === 200 && res.body.success) {
|
||||
res.body.data.images.forEach((item: any) => {
|
||||
result.fullList.push(this.formatFile(item))
|
||||
@@ -127,10 +130,10 @@ class ImgurApi {
|
||||
} else {
|
||||
let initPage = 0
|
||||
do {
|
||||
res = await got(
|
||||
res = (await got(
|
||||
`${this.baseUrl}/account/${this.userName}/images/${initPage}`,
|
||||
getOptions('GET', this.tokenHeaders, undefined, 'json', undefined, undefined, this.proxy)
|
||||
) as any
|
||||
)) as any
|
||||
if (res.statusCode === 200 && res.body.success) {
|
||||
res.body.data.forEach((item: any) => {
|
||||
result.fullList.push(this.formatFile(item))
|
||||
@@ -150,12 +153,12 @@ class ImgurApi {
|
||||
ipcMain.removeAllListeners('cancelLoadingFileList')
|
||||
}
|
||||
|
||||
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async deleteBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { DeleteHash: deleteHash } = configMap
|
||||
const res = await got(
|
||||
const res = (await got(
|
||||
`${this.baseUrl}/account/${this.userName}/image/${deleteHash}`,
|
||||
getOptions('DELETE', this.tokenHeaders, undefined, 'json', undefined, undefined, this.proxy)
|
||||
) as any
|
||||
)) as any
|
||||
return res.statusCode === 200 && res.body.success
|
||||
}
|
||||
|
||||
@@ -163,7 +166,7 @@ class ImgurApi {
|
||||
* 上传文件
|
||||
* @param configMap
|
||||
*/
|
||||
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { fileArray } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
fileArray.forEach((item: any) => {
|
||||
@@ -223,7 +226,7 @@ class ImgurApi {
|
||||
* 下载文件
|
||||
* @param configMap
|
||||
*/
|
||||
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { downloadPath, fileArray, maxDownloadFileCount } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
const promises = [] as any
|
||||
@@ -241,26 +244,21 @@ class ImgurApi {
|
||||
sourceFileName: fileName,
|
||||
targetFilePath: savedFilePath
|
||||
})
|
||||
promises.push(() => new Promise((resolve, reject) => {
|
||||
NewDownloader(
|
||||
instance,
|
||||
url,
|
||||
id,
|
||||
savedFilePath,
|
||||
this.logger,
|
||||
this.proxyStr
|
||||
)
|
||||
.then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
promises.push(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, url, id, savedFilePath, this.logger, this.proxyStr).then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
}
|
||||
const pool = new ConcurrencyPromisePool(maxDownloadFileCount)
|
||||
pool.all(promises).catch((error) => {
|
||||
pool.all(promises).catch(error => {
|
||||
this.logger.error(formatError(error, { class: 'ImgurApi', method: 'downloadBucketFile' }))
|
||||
})
|
||||
return true
|
||||
|
||||
@@ -16,28 +16,30 @@ class LocalApi {
|
||||
logger: ManageLogger
|
||||
isWindows: boolean
|
||||
|
||||
constructor (logger: ManageLogger) {
|
||||
constructor(logger: ManageLogger) {
|
||||
this.logger = logger
|
||||
this.isWindows = process.platform === 'win32'
|
||||
}
|
||||
|
||||
logParam = (error:any, method: string) =>
|
||||
this.logger.error(formatError(error, { class: 'LocalApi', method }))
|
||||
logParam = (error: any, method: string) => this.logger.error(formatError(error, { class: 'LocalApi', method }))
|
||||
|
||||
// windows 系统下将路径转换为 unix 风格
|
||||
transPathToUnix (filePath: string | undefined) {
|
||||
transPathToUnix(filePath: string | undefined) {
|
||||
if (!filePath) return ''
|
||||
return this.isWindows ? filePath.split(path.sep).join(path.posix.sep) : filePath.replace(/^\/+/, '')
|
||||
}
|
||||
|
||||
transBack (filePath: string | undefined) {
|
||||
transBack(filePath: string | undefined) {
|
||||
if (!filePath) return ''
|
||||
return this.isWindows
|
||||
? filePath.split(path.posix.sep).join(path.sep).replace(/^\\+|\\+$/g, '')
|
||||
? filePath
|
||||
.split(path.posix.sep)
|
||||
.join(path.sep)
|
||||
.replace(/^\\+|\\+$/g, '')
|
||||
: `/${filePath.replace(/^\/+|\/+$/g, '')}`
|
||||
}
|
||||
|
||||
formatFolder (item: fs.Stats, urlPrefix: string, fileName: string, filePath: string) {
|
||||
formatFolder(item: fs.Stats, urlPrefix: string, fileName: string, filePath: string) {
|
||||
const key = `${this.transPathToUnix(filePath)}/`.replace(/\/+$/, '/')
|
||||
return {
|
||||
...item,
|
||||
@@ -54,7 +56,7 @@ class LocalApi {
|
||||
}
|
||||
}
|
||||
|
||||
formatFile (item: fs.Stats, urlPrefix: string, fileName: string, filePath: string, isDownload = false) {
|
||||
formatFile(item: fs.Stats, urlPrefix: string, fileName: string, filePath: string, isDownload = false) {
|
||||
const key = isDownload ? filePath : this.transPathToUnix(filePath)
|
||||
return {
|
||||
...item,
|
||||
@@ -71,7 +73,7 @@ class LocalApi {
|
||||
}
|
||||
}
|
||||
|
||||
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListRecursively(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { prefix, customUrl = '', cancelToken } = configMap
|
||||
const urlPrefix = customUrl.replace(/\/+$/, '')
|
||||
@@ -111,7 +113,7 @@ class LocalApi {
|
||||
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
|
||||
}
|
||||
|
||||
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { customUrl = '', cancelToken, baseDir } = configMap
|
||||
let prefix = configMap.prefix
|
||||
@@ -167,7 +169,7 @@ class LocalApi {
|
||||
ipcMain.removeAllListeners('cancelLoadingFileList')
|
||||
}
|
||||
|
||||
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async renameBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { oldKey, newKey } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -179,7 +181,7 @@ class LocalApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async deleteBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -191,7 +193,7 @@ class LocalApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async deleteBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -205,7 +207,7 @@ class LocalApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { fileArray } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
for (const item of fileArray) {
|
||||
@@ -247,7 +249,7 @@ class LocalApi {
|
||||
return true
|
||||
}
|
||||
|
||||
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async createBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -261,7 +263,7 @@ class LocalApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { downloadPath, fileArray } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
for (const item of fileArray) {
|
||||
|
||||
+196
-151
@@ -1,4 +1,3 @@
|
||||
|
||||
import axios from 'axios'
|
||||
import { ipcMain, IpcMainEvent } from 'electron'
|
||||
import path from 'path'
|
||||
@@ -7,7 +6,13 @@ import qiniu from 'qiniu/index'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
|
||||
import UpDownTaskQueue from '~/manage/datastore/upDownTaskQueue'
|
||||
import { hmacSha1Base64, getFileMimeType, NewDownloader, formatError, ConcurrencyPromisePool } from '~/manage/utils/common'
|
||||
import {
|
||||
hmacSha1Base64,
|
||||
getFileMimeType,
|
||||
NewDownloader,
|
||||
formatError,
|
||||
ConcurrencyPromisePool
|
||||
} from '~/manage/utils/common'
|
||||
import { ManageLogger } from '~/manage/utils/logger'
|
||||
|
||||
import { commonTaskStatus, IWindowList, uploadTaskSpecialStatus } from '#/types/enum'
|
||||
@@ -28,14 +33,14 @@ class QiniuApi {
|
||||
getBucketDomain: 'https://uc.qiniuapi.com/v2/domains'
|
||||
}
|
||||
|
||||
constructor (accessKey: string, secretKey: string, logger: ManageLogger) {
|
||||
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, urlPrefix: string) {
|
||||
formatFolder(item: string, slicedPrefix: string, urlPrefix: string) {
|
||||
return {
|
||||
Key: item,
|
||||
key: item,
|
||||
@@ -49,7 +54,7 @@ class QiniuApi {
|
||||
}
|
||||
}
|
||||
|
||||
formatFile (item: any, slicedPrefix: string, urlPrefix: string) {
|
||||
formatFile(item: any, slicedPrefix: string, urlPrefix: string) {
|
||||
const fileName = item.key.replace(slicedPrefix, '')
|
||||
return {
|
||||
...item,
|
||||
@@ -64,7 +69,7 @@ class QiniuApi {
|
||||
}
|
||||
}
|
||||
|
||||
authorization (
|
||||
authorization(
|
||||
method: string,
|
||||
urlPath: string,
|
||||
host: string,
|
||||
@@ -79,7 +84,7 @@ class QiniuApi {
|
||||
if (xQiniuHeaders) {
|
||||
const xQiniuHeaderStr = Object.keys(xQiniuHeaders)
|
||||
.sort()
|
||||
.map((key) => `\n${key}:${xQiniuHeaders[key]}`)
|
||||
.map(key => `\n${key}:${xQiniuHeaders[key]}`)
|
||||
.join('')
|
||||
signStr += xQiniuHeaderStr
|
||||
}
|
||||
@@ -92,8 +97,8 @@ class QiniuApi {
|
||||
|
||||
/**
|
||||
* 获取存储桶列表
|
||||
*/
|
||||
async getBucketList (): Promise<any> {
|
||||
*/
|
||||
async getBucketList(): Promise<any> {
|
||||
const host = this.hostList.getBucketList
|
||||
const authorization = qiniu.util.generateAccessToken(this.mac, host, undefined)
|
||||
const res = await axios.get(host, {
|
||||
@@ -122,8 +127,8 @@ class QiniuApi {
|
||||
|
||||
/**
|
||||
* 获取存储桶详细信息
|
||||
*/
|
||||
async getBucketInfo (param: IStringKeyMap): Promise<any> {
|
||||
*/
|
||||
async getBucketInfo(param: IStringKeyMap): Promise<any> {
|
||||
const { bucketName } = param
|
||||
const urlPath = `/v2/bucketInfo?bucket=${bucketName}&fs=true`
|
||||
const authorization = this.authorization('POST', urlPath, this.host, '', '', 'application/json')
|
||||
@@ -143,19 +148,19 @@ class QiniuApi {
|
||||
})
|
||||
return res?.status === 200
|
||||
? {
|
||||
success: true,
|
||||
private: res.data.private,
|
||||
zone: res.data.zone
|
||||
}
|
||||
success: true,
|
||||
private: res.data.private,
|
||||
zone: res.data.zone
|
||||
}
|
||||
: {
|
||||
success: false
|
||||
}
|
||||
success: false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义域名
|
||||
*/
|
||||
async getBucketDomain (param: IStringKeyMap): Promise<any> {
|
||||
async getBucketDomain(param: IStringKeyMap): Promise<any> {
|
||||
const { bucketName } = param
|
||||
const host = this.hostList.getBucketDomain
|
||||
const authorization = qiniu.util.generateAccessToken(this.mac, `${host}?tbl=${bucketName}`, undefined)
|
||||
@@ -175,7 +180,7 @@ class QiniuApi {
|
||||
/**
|
||||
* 修改存储桶权限
|
||||
*/
|
||||
async setBucketAclPolicy (param: IStringKeyMap): Promise<boolean> {
|
||||
async setBucketAclPolicy(param: IStringKeyMap): Promise<boolean> {
|
||||
// 0: 公开访问 1: 私有访问
|
||||
const { bucketName } = param
|
||||
let { isPrivate } = param
|
||||
@@ -207,8 +212,8 @@ class QiniuApi {
|
||||
* region: string,
|
||||
* acl: boolean // 是否公开访问
|
||||
* }
|
||||
*/
|
||||
async createBucket (configMap: IStringKeyMap): Promise<boolean> {
|
||||
*/
|
||||
async createBucket(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { BucketName, region, acl } = configMap
|
||||
const urlPath = `/mkbucketv3/${BucketName}/region/${region}`
|
||||
const authorization = this.authorization('POST', urlPath, this.host, '', '', 'application/json')
|
||||
@@ -224,13 +229,13 @@ class QiniuApi {
|
||||
})
|
||||
return res?.status === 200
|
||||
? await this.setBucketAclPolicy({
|
||||
bucketName: BucketName,
|
||||
isPrivate: !acl
|
||||
})
|
||||
bucketName: BucketName,
|
||||
isPrivate: !acl
|
||||
})
|
||||
: false
|
||||
}
|
||||
|
||||
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListRecursively(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: bucket, prefix, cancelToken, customUrl: urlPrefix } = configMap
|
||||
let marker = undefined as any
|
||||
@@ -252,25 +257,31 @@ class QiniuApi {
|
||||
const bucketManager = new qiniu.rs.BucketManager(this.mac, config)
|
||||
do {
|
||||
res = await new Promise((resolve, reject) => {
|
||||
bucketManager.listPrefix(bucket, {
|
||||
prefix: slicedPrefix === '' ? undefined : slicedPrefix,
|
||||
marker,
|
||||
limit: 1000
|
||||
}, (err: any, respBody: any, respInfo: any) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve({
|
||||
respBody,
|
||||
respInfo
|
||||
})
|
||||
bucketManager.listPrefix(
|
||||
bucket,
|
||||
{
|
||||
prefix: slicedPrefix === '' ? undefined : slicedPrefix,
|
||||
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.items && res.respBody.items.forEach((item: any) => {
|
||||
item.fsize !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
|
||||
})
|
||||
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(refreshDownloadFileTransferList, result)
|
||||
} else {
|
||||
result.finished = true
|
||||
@@ -286,7 +297,7 @@ class QiniuApi {
|
||||
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
|
||||
}
|
||||
|
||||
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: bucket, prefix, cancelToken, customUrl: urlPrefix } = configMap
|
||||
let marker = undefined as any
|
||||
@@ -308,29 +319,37 @@ class QiniuApi {
|
||||
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
|
||||
})
|
||||
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, urlPrefix))
|
||||
})
|
||||
res.respBody && res.respBody.items && res.respBody.items.forEach((item: any) => {
|
||||
item.fsize !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
|
||||
})
|
||||
res.respBody &&
|
||||
res.respBody.commonPrefixes &&
|
||||
res.respBody.commonPrefixes.forEach((item: any) => {
|
||||
result.fullList.push(this.formatFolder(item, slicedPrefix, urlPrefix))
|
||||
})
|
||||
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
|
||||
@@ -360,8 +379,8 @@ class QiniuApi {
|
||||
* itemsPerPage: number,
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getBucketFileList (configMap: IStringKeyMap): Promise<any> {
|
||||
*/
|
||||
async getBucketFileList(configMap: IStringKeyMap): Promise<any> {
|
||||
const { bucketName: bucket, prefix, marker, itemsPerPage, customUrl: urlPrefix } = configMap
|
||||
const slicedPrefix = prefix.slice(1)
|
||||
const config = new qiniu.conf.Config()
|
||||
@@ -374,21 +393,25 @@ class QiniuApi {
|
||||
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
|
||||
})
|
||||
bucketManager.listPrefix(
|
||||
bucket,
|
||||
{
|
||||
limit: itemsPerPage,
|
||||
prefix: slicedPrefix === '' ? undefined : slicedPrefix,
|
||||
marker,
|
||||
delimiter: '/'
|
||||
},
|
||||
(err, respBody, respInfo) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve({
|
||||
respBody,
|
||||
respInfo
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
if (res?.respInfo?.statusCode === 200) {
|
||||
if (res.respBody?.commonPrefixes) {
|
||||
@@ -401,7 +424,7 @@ class QiniuApi {
|
||||
item.fsize !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
|
||||
})
|
||||
}
|
||||
result.isTruncated = !!(res.respBody?.marker)
|
||||
result.isTruncated = !!res.respBody?.marker
|
||||
result.nextMarker = res.respBody?.marker ? res.respBody.marker : ''
|
||||
result.success = true
|
||||
}
|
||||
@@ -409,19 +432,19 @@ class QiniuApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
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) => {
|
||||
const res = (await new Promise((resolve, reject) => {
|
||||
bucketManager.delete(bucketName, key, (err, respBody, respInfo) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
@@ -432,7 +455,7 @@ class QiniuApi {
|
||||
})
|
||||
}
|
||||
})
|
||||
}) as any
|
||||
})) as any
|
||||
return res?.respInfo?.statusCode === 200
|
||||
}
|
||||
|
||||
@@ -440,7 +463,7 @@ class QiniuApi {
|
||||
* 删除文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async deleteBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, key } = configMap
|
||||
const config = new qiniu.conf.Config()
|
||||
const bucketManager = new qiniu.rs.BucketManager(this.mac, config)
|
||||
@@ -450,27 +473,31 @@ class QiniuApi {
|
||||
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
|
||||
})
|
||||
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
|
||||
)
|
||||
})) as any
|
||||
if (res?.respInfo?.statusCode === 200) {
|
||||
if (res.respBody?.items) {
|
||||
allFileList.Contents = allFileList.Contents.concat(res.respBody.items)
|
||||
}
|
||||
isTruncated = !!(res.respBody?.marker)
|
||||
isTruncated = !!res.respBody?.marker
|
||||
marker = res.respBody?.marker ? res.respBody.marker : ''
|
||||
} else {
|
||||
return false
|
||||
@@ -481,7 +508,7 @@ class QiniuApi {
|
||||
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) => {
|
||||
const res = (await new Promise((resolve, reject) => {
|
||||
bucketManager.batch(deleteOps, (err, respBody, respInfo) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
@@ -492,40 +519,47 @@ class QiniuApi {
|
||||
})
|
||||
}
|
||||
})
|
||||
}) as any
|
||||
})) as any
|
||||
if (res?.respInfo?.statusCode !== 200) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* oldKey: string,
|
||||
* newKey: string
|
||||
* }
|
||||
*/
|
||||
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 重命名文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* oldKey: string,
|
||||
* newKey: string
|
||||
* }
|
||||
*/
|
||||
async renameBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
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
|
||||
})
|
||||
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
|
||||
)
|
||||
})) as any
|
||||
return res?.respInfo?.statusCode === 200
|
||||
}
|
||||
|
||||
@@ -540,7 +574,7 @@ class QiniuApi {
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
|
||||
async getPreSignedUrl(configMap: IStringKeyMap): Promise<string> {
|
||||
const { key, expires, customUrl } = configMap
|
||||
const config = new qiniu.conf.Config()
|
||||
const bucketManager = new qiniu.rs.BucketManager(this.mac, config)
|
||||
@@ -554,7 +588,7 @@ class QiniuApi {
|
||||
* 上传文件
|
||||
* @param configMap
|
||||
*/
|
||||
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { fileArray } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
fileArray.forEach((item: any) => {
|
||||
@@ -585,7 +619,7 @@ class QiniuApi {
|
||||
putExtra.version = 'v2'
|
||||
putExtra.partSize = 4 * 1024 * 1024
|
||||
putExtra.progressCallback = (uploadBytes, totalBytes) => {
|
||||
const progress = Math.floor(uploadBytes / totalBytes * 100)
|
||||
const progress = Math.floor((uploadBytes / totalBytes) * 100)
|
||||
instance.updateUploadTask({
|
||||
id: `${bucketName}-${region}-${key}-${filePath}`,
|
||||
progress,
|
||||
@@ -594,7 +628,12 @@ class QiniuApi {
|
||||
}
|
||||
resumeUploader.putFile(uploadToken, key, filePath, putExtra, (respErr, respBody, respInfo) => {
|
||||
if (respErr) {
|
||||
this.logger.error(formatError(respErr, { class: 'Qiniu', method: 'uploadBucketFile' }))
|
||||
this.logger.error(
|
||||
formatError(respErr, {
|
||||
class: 'Qiniu',
|
||||
method: 'uploadBucketFile'
|
||||
})
|
||||
)
|
||||
instance.updateUploadTask({
|
||||
id: `${bucketName}-${region}-${key}-${filePath}`,
|
||||
progress: 0,
|
||||
@@ -628,7 +667,7 @@ class QiniuApi {
|
||||
* 新建文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async createBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, key } = configMap
|
||||
const putPolicy = new qiniu.rs.PutPolicy({
|
||||
scope: `${bucketName}:${key}`
|
||||
@@ -636,7 +675,7 @@ class QiniuApi {
|
||||
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) => {
|
||||
const res = (await new Promise((resolve, reject) => {
|
||||
FormUploader.put(uploadToken, key, '', putExtra, (err, respBody, respInfo) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
@@ -647,7 +686,7 @@ class QiniuApi {
|
||||
})
|
||||
}
|
||||
})
|
||||
}) as any
|
||||
})) as any
|
||||
return res?.respInfo?.statusCode === 200
|
||||
}
|
||||
|
||||
@@ -655,7 +694,7 @@ class QiniuApi {
|
||||
* 下载文件
|
||||
* @param configMap
|
||||
*/
|
||||
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { downloadPath, fileArray, maxDownloadFileCount } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
const promises = [] as any
|
||||
@@ -673,20 +712,26 @@ class QiniuApi {
|
||||
sourceFileName: fileName,
|
||||
targetFilePath: savedFilePath
|
||||
})
|
||||
const preSignedUrl = await this.getPreSignedUrl({ key, expires: 36000, customUrl })
|
||||
promises.push(() => new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger)
|
||||
.then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
const preSignedUrl = await this.getPreSignedUrl({
|
||||
key,
|
||||
expires: 36000,
|
||||
customUrl
|
||||
})
|
||||
promises.push(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger).then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
}
|
||||
const pool = new ConcurrencyPromisePool(maxDownloadFileCount)
|
||||
pool.all(promises).catch((error) => {
|
||||
pool.all(promises).catch(error => {
|
||||
this.logger.error(formatError(error, { class: 'QiniuApi', method: 'downloadBucketFile' }))
|
||||
})
|
||||
return true
|
||||
|
||||
+184
-138
@@ -46,7 +46,7 @@ class S3plistApi {
|
||||
secretAccessKey: string
|
||||
bucketName: string
|
||||
|
||||
constructor (
|
||||
constructor(
|
||||
accessKeyId: string,
|
||||
secretAccessKey: string,
|
||||
endpoint: string | undefined,
|
||||
@@ -75,9 +75,9 @@ class S3plistApi {
|
||||
this.proxy = formatHttpProxy(proxy, 'string') as string | undefined
|
||||
}
|
||||
|
||||
async getDogeCloudToken () {
|
||||
async getDogeCloudToken() {
|
||||
if (!this.dogeCloudSupport) return
|
||||
const token = await getTempToken(this.accessKeyId, this.secretAccessKey) as DogecloudToken
|
||||
const token = (await getTempToken(this.accessKeyId, this.secretAccessKey)) as DogecloudToken
|
||||
if (Object.keys(token).length === 0) {
|
||||
throw new Error('manage.setting.dogeCloudTokenError')
|
||||
}
|
||||
@@ -88,7 +88,7 @@ class S3plistApi {
|
||||
}
|
||||
}
|
||||
|
||||
setAgent (proxy: string | undefined, sslEnabled: boolean) : NodeHttpHandler {
|
||||
setAgent(proxy: string | undefined, sslEnabled: boolean): NodeHttpHandler {
|
||||
const agent = getAgent(proxy, sslEnabled)
|
||||
const commonOptions: AgentOptions = {
|
||||
keepAlive: true,
|
||||
@@ -98,27 +98,26 @@ class S3plistApi {
|
||||
const extraOptions = sslEnabled ? { rejectUnauthorized: false } : {}
|
||||
return sslEnabled
|
||||
? new NodeHttpHandler({
|
||||
httpsAgent: agent.https
|
||||
? agent.https
|
||||
: new https.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
httpsAgent: agent.https
|
||||
? agent.https
|
||||
: new https.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
: new NodeHttpHandler({
|
||||
httpAgent: agent.http
|
||||
? agent.http
|
||||
: new http.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
httpAgent: agent.http
|
||||
? agent.http
|
||||
: new http.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
logParam = (error:any, method: string) =>
|
||||
this.logger.error(formatError(error, { class: 'S3plistApi', method }))
|
||||
logParam = (error: any, method: string) => this.logger.error(formatError(error, { class: 'S3plistApi', method }))
|
||||
|
||||
formatFolder (item: CommonPrefix, slicedPrefix: string, urlPrefix: string): any {
|
||||
formatFolder(item: CommonPrefix, slicedPrefix: string, urlPrefix: string): any {
|
||||
return {
|
||||
Key: item.Prefix,
|
||||
url: `${urlPrefix}/${item.Prefix}`,
|
||||
@@ -133,7 +132,7 @@ class S3plistApi {
|
||||
}
|
||||
}
|
||||
|
||||
formatFile (item: _Object, slicedPrefix: string, urlPrefix: string): any {
|
||||
formatFile(item: _Object, slicedPrefix: string, urlPrefix: string): any {
|
||||
const fileName = item.Key?.replace(slicedPrefix, '')
|
||||
return {
|
||||
...item,
|
||||
@@ -149,7 +148,7 @@ class S3plistApi {
|
||||
}
|
||||
}
|
||||
|
||||
async putPublicAccess (bucketName: string, client: S3Client) {
|
||||
async putPublicAccess(bucketName: string, client: S3Client) {
|
||||
const input = {
|
||||
Bucket: bucketName,
|
||||
PublicAccessBlockConfiguration: {
|
||||
@@ -170,13 +169,13 @@ class S3plistApi {
|
||||
/**
|
||||
* 新建存储桶
|
||||
* @param {Object} configMap
|
||||
* configMap = {
|
||||
* BucketName: string,
|
||||
* region: string,
|
||||
* acl: string
|
||||
* }
|
||||
* configMap = {
|
||||
* BucketName: string,
|
||||
* region: string,
|
||||
* acl: string
|
||||
* }
|
||||
*/
|
||||
async createBucket (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async createBucket(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { BucketName, region, acl, endpoint } = configMap
|
||||
try {
|
||||
await this.getDogeCloudToken()
|
||||
@@ -186,7 +185,7 @@ class S3plistApi {
|
||||
const command = new ListBucketsCommand({})
|
||||
const data = await client.send(command)
|
||||
if (data.$metadata.httpStatusCode === 200) {
|
||||
const bucketList = data.Buckets?.map((item) => item.Name)
|
||||
const bucketList = data.Buckets?.map(item => item.Name)
|
||||
if (bucketList?.includes(BucketName)) {
|
||||
return true
|
||||
}
|
||||
@@ -233,9 +232,9 @@ class S3plistApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储桶列表
|
||||
*/
|
||||
async getBucketList (): Promise<any> {
|
||||
* 获取存储桶列表
|
||||
*/
|
||||
async getBucketList(): Promise<any> {
|
||||
if (this.dogeCloudSupport) {
|
||||
try {
|
||||
const res = await dogecloudApi('/oss/bucket/list.json', {}, false, this.accessKeyId, this.secretAccessKey)
|
||||
@@ -258,7 +257,7 @@ class S3plistApi {
|
||||
}
|
||||
const options = Object.assign({}, this.baseOptions) as S3ClientConfig
|
||||
const result: IStringKeyMap[] = []
|
||||
const endpoint = options.endpoint as string || ''
|
||||
const endpoint = (options.endpoint as string) || ''
|
||||
options.region = endpoint.includes('cloudflarestorage') ? 'auto' : 'us-east-1'
|
||||
try {
|
||||
const client = new S3Client(options)
|
||||
@@ -271,23 +270,28 @@ class S3plistApi {
|
||||
|
||||
if (data.Buckets) {
|
||||
if (endpoint.includes('cloudflarestorage')) {
|
||||
result.push(...data.Buckets.map(bucket => ({
|
||||
Name: bucket.Name,
|
||||
CreationDate: bucket.CreationDate,
|
||||
Location: 'auto'
|
||||
})))
|
||||
result.push(
|
||||
...data.Buckets.map(bucket => ({
|
||||
Name: bucket.Name,
|
||||
CreationDate: bucket.CreationDate,
|
||||
Location: 'auto'
|
||||
}))
|
||||
)
|
||||
} else {
|
||||
for (const bucket of data.Buckets) {
|
||||
const bucketName = bucket.Name
|
||||
const bucketConfig = await client.send(new GetBucketLocationCommand({
|
||||
Bucket: bucketName
|
||||
}))
|
||||
const bucketConfig = await client.send(
|
||||
new GetBucketLocationCommand({
|
||||
Bucket: bucketName
|
||||
})
|
||||
)
|
||||
result.push({
|
||||
Name: bucketName,
|
||||
CreationDate: bucket.CreationDate,
|
||||
Location: bucketConfig.$metadata.httpStatusCode === 200
|
||||
? bucketConfig.LocationConstraint?.toLowerCase() || 'us-east-1'
|
||||
: 'us-east-1'
|
||||
Location:
|
||||
bucketConfig.$metadata.httpStatusCode === 200
|
||||
? bucketConfig.LocationConstraint?.toLowerCase() || 'us-east-1'
|
||||
: 'us-east-1'
|
||||
})
|
||||
if (bucketConfig.$metadata.httpStatusCode !== 200) {
|
||||
this.logParam(bucketConfig, 'getBucketList')
|
||||
@@ -301,9 +305,14 @@ class S3plistApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListRecursively(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, cancelToken } = configMap
|
||||
const {
|
||||
bucketName: bucket,
|
||||
bucketConfig: { Location: region },
|
||||
prefix,
|
||||
cancelToken
|
||||
} = configMap
|
||||
const slicedPrefix = prefix.slice(1)
|
||||
const urlPrefix = configMap.customUrl || `https://${bucket}.s3.amazonaws.com`
|
||||
let marker
|
||||
@@ -333,9 +342,10 @@ class S3plistApi {
|
||||
})
|
||||
res = await client.send(command)
|
||||
if (res.$metadata.httpStatusCode === 200) {
|
||||
res.Contents && res.Contents.forEach((item: _Object) => {
|
||||
result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
|
||||
})
|
||||
res.Contents &&
|
||||
res.Contents.forEach((item: _Object) => {
|
||||
result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
|
||||
})
|
||||
window.webContents.send(refreshDownloadFileTransferList, result)
|
||||
} else {
|
||||
this.logParam(res, 'getBucketListRecursively')
|
||||
@@ -359,9 +369,14 @@ class S3plistApi {
|
||||
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
|
||||
}
|
||||
|
||||
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, cancelToken } = configMap
|
||||
const {
|
||||
bucketName: bucket,
|
||||
bucketConfig: { Location: region },
|
||||
prefix,
|
||||
cancelToken
|
||||
} = configMap
|
||||
const slicedPrefix = prefix.slice(1)
|
||||
const urlPrefix = configMap.customUrl || `https://${bucket}.s3.amazonaws.com`
|
||||
let marker
|
||||
@@ -393,12 +408,14 @@ class S3plistApi {
|
||||
})
|
||||
res = await client.send(command)
|
||||
if (res.$metadata.httpStatusCode === 200) {
|
||||
res.CommonPrefixes && res.CommonPrefixes.forEach((item: CommonPrefix) => {
|
||||
result.fullList.push(this.formatFolder(item, slicedPrefix, urlPrefix))
|
||||
})
|
||||
res.Contents && res.Contents.forEach((item: _Object) => {
|
||||
result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
|
||||
})
|
||||
res.CommonPrefixes &&
|
||||
res.CommonPrefixes.forEach((item: CommonPrefix) => {
|
||||
result.fullList.push(this.formatFolder(item, slicedPrefix, urlPrefix))
|
||||
})
|
||||
res.Contents &&
|
||||
res.Contents.forEach((item: _Object) => {
|
||||
result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
|
||||
})
|
||||
window.webContents.send('refreshFileTransferList', result)
|
||||
} else {
|
||||
this.logParam(res, 'getBucketListBackstage')
|
||||
@@ -422,8 +439,14 @@ class S3plistApi {
|
||||
ipcMain.removeAllListeners('cancelLoadingFileList')
|
||||
}
|
||||
|
||||
async getBucketFileList (configMap: IStringKeyMap): Promise<any> {
|
||||
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, marker, itemsPerPage } = configMap
|
||||
async getBucketFileList(configMap: IStringKeyMap): Promise<any> {
|
||||
const {
|
||||
bucketName: bucket,
|
||||
bucketConfig: { Location: region },
|
||||
prefix,
|
||||
marker,
|
||||
itemsPerPage
|
||||
} = configMap
|
||||
const slicedPrefix = prefix.slice(1)
|
||||
const urlPrefix = configMap.customUrl || `https://${bucket}.s3.amazonaws.com`
|
||||
const result = {
|
||||
@@ -434,7 +457,10 @@ class S3plistApi {
|
||||
}
|
||||
try {
|
||||
await this.getDogeCloudToken()
|
||||
const options = Object.assign({}, { ...this.baseOptions, region: String(region) || 'us-east-1' }) as S3ClientConfig
|
||||
const options = Object.assign(
|
||||
{},
|
||||
{ ...this.baseOptions, region: String(region) || 'us-east-1' }
|
||||
) as S3ClientConfig
|
||||
const client = new S3Client(options)
|
||||
const command = new ListObjectsV2Command({
|
||||
Bucket: bucket,
|
||||
@@ -460,21 +486,24 @@ class S3plistApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* oldKey: string,
|
||||
* newKey: string
|
||||
* }
|
||||
*/
|
||||
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 重命名文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* oldKey: string,
|
||||
* newKey: string
|
||||
* }
|
||||
*/
|
||||
async renameBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, oldKey, newKey } = configMap
|
||||
let result = false
|
||||
try {
|
||||
await this.getDogeCloudToken()
|
||||
const options = Object.assign({}, { ...this.baseOptions, region: String(region) || 'us-east-1' }) as S3ClientConfig
|
||||
const options = Object.assign(
|
||||
{},
|
||||
{ ...this.baseOptions, region: String(region) || 'us-east-1' }
|
||||
) as S3ClientConfig
|
||||
const client = new S3Client(options)
|
||||
const command = new CopyObjectCommand({
|
||||
Bucket: bucketName,
|
||||
@@ -503,15 +532,15 @@ class S3plistApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, key } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -536,10 +565,10 @@ class S3plistApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 删除文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async deleteBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, key } = configMap
|
||||
let marker
|
||||
let result = false
|
||||
@@ -562,7 +591,7 @@ class S3plistApi {
|
||||
Delimiter: '/',
|
||||
MaxKeys: 1000
|
||||
})
|
||||
res = await client.send(command) as ListObjectsV2CommandOutput
|
||||
res = (await client.send(command)) as ListObjectsV2CommandOutput
|
||||
if (res.$metadata.httpStatusCode === 200) {
|
||||
res.CommonPrefixes && allFileList.CommonPrefixes.push(...res.CommonPrefixes)
|
||||
res.Contents && allFileList.Contents.push(...res.Contents)
|
||||
@@ -595,7 +624,7 @@ class S3plistApi {
|
||||
const deleteCommand = new DeleteObjectsCommand({
|
||||
Bucket: bucketName,
|
||||
Delete: {
|
||||
Objects: deleteList.map((item) => {
|
||||
Objects: deleteList.map(item => {
|
||||
return {
|
||||
Key: item.Key
|
||||
}
|
||||
@@ -618,29 +647,33 @@ class S3plistApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预签名url
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string,
|
||||
* expires: number,
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
|
||||
* 获取预签名url
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string,
|
||||
* expires: number,
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getPreSignedUrl(configMap: IStringKeyMap): Promise<string> {
|
||||
const { bucketName, region, key, expires } = configMap
|
||||
try {
|
||||
await this.getDogeCloudToken()
|
||||
const options = Object.assign({}, this.baseOptions) as S3ClientConfig
|
||||
options.region = String(region) || 'us-east-1'
|
||||
const client = new S3Client(options)
|
||||
const signedUrl = await getSignedUrl(client, new GetObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: key
|
||||
}), {
|
||||
expiresIn: expires || 3600
|
||||
})
|
||||
const signedUrl = await getSignedUrl(
|
||||
client,
|
||||
new GetObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: key
|
||||
}),
|
||||
{
|
||||
expiresIn: expires || 3600
|
||||
}
|
||||
)
|
||||
return signedUrl
|
||||
} catch (error) {
|
||||
this.logParam(error, 'getPreSignedUrl')
|
||||
@@ -652,7 +685,7 @@ class S3plistApi {
|
||||
* 新建文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async createBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, key } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -680,7 +713,7 @@ class S3plistApi {
|
||||
* upload file
|
||||
* @param configMap
|
||||
*/
|
||||
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { fileArray } = configMap
|
||||
// fileArray = [{
|
||||
// bucketName: string,
|
||||
@@ -693,7 +726,15 @@ class S3plistApi {
|
||||
fileArray.forEach((item: any) => {
|
||||
item.key.startsWith('/') && (item.key = item.key.slice(1))
|
||||
})
|
||||
const allowedAcl = ['private', 'public-read', 'public-read-write', 'aws-exec-read', 'authenticated-read', 'bucket-owner-read', 'bucket-owner-full-control']
|
||||
const allowedAcl = [
|
||||
'private',
|
||||
'public-read',
|
||||
'public-read-write',
|
||||
'aws-exec-read',
|
||||
'authenticated-read',
|
||||
'bucket-owner-read',
|
||||
'bucket-owner-full-control'
|
||||
]
|
||||
for (const item of fileArray) {
|
||||
const { bucketName, region, key, filePath, fileName, aclForUpload } = item
|
||||
const id = `${bucketName}-${String(region)}-${key}-${filePath}`
|
||||
@@ -743,36 +784,39 @@ class S3plistApi {
|
||||
parallelUploads3.on('httpUploadProgress', (progress: Progress) => {
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: progress.loaded && progress.total ? Math.floor(progress.loaded / progress.total * 100) : 0,
|
||||
progress: progress.loaded && progress.total ? Math.floor((progress.loaded / progress.total) * 100) : 0,
|
||||
status: uploadTaskSpecialStatus.uploading
|
||||
})
|
||||
})
|
||||
parallelUploads3.done().then((data) => {
|
||||
if (data.$metadata.httpStatusCode === 200) {
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 100,
|
||||
status: uploadTaskSpecialStatus.uploaded,
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
} else {
|
||||
parallelUploads3
|
||||
.done()
|
||||
.then(data => {
|
||||
if (data.$metadata.httpStatusCode === 200) {
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 100,
|
||||
status: uploadTaskSpecialStatus.uploaded,
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
} else {
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 0,
|
||||
status: commonTaskStatus.failed,
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.logParam(error, 'uploadBucketFile')
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 0,
|
||||
status: commonTaskStatus.failed,
|
||||
response: JSON.stringify(error),
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.logParam(error, 'uploadBucketFile')
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 0,
|
||||
status: commonTaskStatus.failed,
|
||||
response: JSON.stringify(error),
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -781,7 +825,7 @@ class S3plistApi {
|
||||
* 下载文件
|
||||
* @param configMap
|
||||
*/
|
||||
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { downloadPath, fileArray, maxDownloadFileCount } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
const promises = [] as any
|
||||
@@ -806,19 +850,21 @@ class S3plistApi {
|
||||
expires: 36000,
|
||||
customUrl
|
||||
})
|
||||
promises.push(() => new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger, this.proxy)
|
||||
.then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
promises.push(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger, this.proxy).then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
}
|
||||
const pool = new ConcurrencyPromisePool(maxDownloadFileCount)
|
||||
pool.all(promises).catch((error) => {
|
||||
pool.all(promises).catch(error => {
|
||||
this.logParam(error, 'downloadBucketFile')
|
||||
})
|
||||
return true
|
||||
|
||||
@@ -45,7 +45,7 @@ class SftpApi {
|
||||
passphrase: string
|
||||
}
|
||||
|
||||
constructor (
|
||||
constructor(
|
||||
host: string,
|
||||
port: Undefinable<number>,
|
||||
username: Undefinable<string>,
|
||||
@@ -76,8 +76,7 @@ class SftpApi {
|
||||
}
|
||||
}
|
||||
|
||||
logParam = (error:any, method: string) =>
|
||||
this.logger.error(formatError(error, { class: 'SftpApi', method }))
|
||||
logParam = (error: any, method: string) => this.logger.error(formatError(error, { class: 'SftpApi', method }))
|
||||
|
||||
transFormPermission = (permissionsStr: string) => {
|
||||
const permissions = permissionsStr.length === 10 ? permissionsStr.slice(1) : permissionsStr
|
||||
@@ -96,7 +95,7 @@ class SftpApi {
|
||||
return `0${result}`
|
||||
}
|
||||
|
||||
formatFolder (item: listDirResult, urlPrefix: string, isWebPath = false) {
|
||||
formatFolder(item: listDirResult, urlPrefix: string, isWebPath = false) {
|
||||
const key = item.key
|
||||
let url: string
|
||||
if (isWebPath) {
|
||||
@@ -123,7 +122,7 @@ class SftpApi {
|
||||
}
|
||||
}
|
||||
|
||||
formatFile (item: listDirResult, urlPrefix: string, isWebPath = false) {
|
||||
formatFile(item: listDirResult, urlPrefix: string, isWebPath = false) {
|
||||
const key = item.key
|
||||
return {
|
||||
...item,
|
||||
@@ -153,7 +152,7 @@ class SftpApi {
|
||||
}
|
||||
}
|
||||
|
||||
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListRecursively(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { prefix, customUrl, cancelToken } = configMap
|
||||
const urlPrefix = customUrl || `${this.host}:${this.port}`
|
||||
@@ -193,7 +192,7 @@ class SftpApi {
|
||||
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
|
||||
}
|
||||
|
||||
formatLSResult (res: string, cwd: string): listDirResult[] {
|
||||
formatLSResult(res: string, cwd: string): listDirResult[] {
|
||||
const result = [] as listDirResult[]
|
||||
const resArray = res.trim().split('\n')
|
||||
resArray.slice(resArray[0].startsWith('total') ? 1 : 0).forEach((item: string) => {
|
||||
@@ -219,7 +218,7 @@ class SftpApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { prefix, customUrl, cancelToken, baseDir } = configMap
|
||||
let urlPrefix = customUrl || `${this.host}:${this.port}`
|
||||
@@ -250,7 +249,8 @@ class SftpApi {
|
||||
if (formatedLSRes.length) {
|
||||
formatedLSRes.forEach((item: listDirResult) => {
|
||||
const relativePath = path.relative(baseDir, item.key.startsWith('/') ? item.key : `/${item.key}`)
|
||||
const relative = webPath && urlPrefix + `/${path.join(webPath, relativePath)}`.replace(/\\/g, '/').replace(/\/+/g, '/')
|
||||
const relative =
|
||||
webPath && urlPrefix + `/${path.join(webPath, relativePath)}`.replace(/\\/g, '/').replace(/\/+/g, '/')
|
||||
if (item.isDir) {
|
||||
result.fullList.push(this.formatFolder(item, webPath ? relative : urlPrefix, !!webPath))
|
||||
} else {
|
||||
@@ -277,7 +277,7 @@ class SftpApi {
|
||||
ipcMain.removeAllListeners('cancelLoadingFileList')
|
||||
}
|
||||
|
||||
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async renameBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { oldKey, newKey } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -291,7 +291,7 @@ class SftpApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async deleteBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -305,7 +305,7 @@ class SftpApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async deleteBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -322,7 +322,7 @@ class SftpApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { fileArray } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
for (const item of fileArray) {
|
||||
@@ -377,7 +377,7 @@ class SftpApi {
|
||||
return true
|
||||
}
|
||||
|
||||
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async createBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -391,7 +391,7 @@ class SftpApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { downloadPath, fileArray } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
for (const item of fileArray) {
|
||||
|
||||
@@ -20,7 +20,7 @@ class SmmsApi {
|
||||
logger: ManageLogger
|
||||
timeout = 30000
|
||||
|
||||
constructor (token: string, logger: ManageLogger) {
|
||||
constructor(token: string, logger: ManageLogger) {
|
||||
this.token = token
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: this.baseUrl,
|
||||
@@ -36,7 +36,7 @@ class SmmsApi {
|
||||
this.logger = logger
|
||||
}
|
||||
|
||||
formatFile (item: any) {
|
||||
formatFile(item: any) {
|
||||
return {
|
||||
...item,
|
||||
Key: item.path,
|
||||
@@ -53,7 +53,7 @@ class SmmsApi {
|
||||
}
|
||||
}
|
||||
|
||||
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { cancelToken } = configMap
|
||||
let marker = 1
|
||||
@@ -71,17 +71,15 @@ class SmmsApi {
|
||||
finished: false
|
||||
}
|
||||
do {
|
||||
res = await this.axiosInstance(
|
||||
'/upload_history',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
params: {
|
||||
page: marker
|
||||
}
|
||||
})
|
||||
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
|
||||
@@ -123,26 +121,23 @@ class SmmsApi {
|
||||
* itemsPerPage: number,
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getBucketFileList ({ currentPage }: IStringKeyMap): Promise<any> {
|
||||
*/
|
||||
async getBucketFileList({ currentPage }: IStringKeyMap): Promise<any> {
|
||||
const result = {
|
||||
fullList: <any>[],
|
||||
isTruncated: false,
|
||||
nextMarker: '',
|
||||
success: false
|
||||
}
|
||||
const res = await this.axiosInstance(
|
||||
'/upload_history',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
params: {
|
||||
page: currentPage
|
||||
}
|
||||
const res = await this.axiosInstance('/upload_history', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
params: {
|
||||
page: currentPage
|
||||
}
|
||||
)
|
||||
})
|
||||
if (res?.status !== 200 || !res?.data?.success) return result
|
||||
|
||||
if (res.data.Count === 0) return { ...result, success: true }
|
||||
@@ -157,26 +152,23 @@ class SmmsApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string,
|
||||
* DeleteHash: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile ({ DeleteHash }: IStringKeyMap): Promise<boolean> {
|
||||
const res = await this.axiosInstance(
|
||||
`/delete/${DeleteHash}`,
|
||||
{
|
||||
method: 'GET',
|
||||
params: {
|
||||
hash: DeleteHash,
|
||||
format: 'json'
|
||||
}
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string,
|
||||
* DeleteHash: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile({ DeleteHash }: IStringKeyMap): Promise<boolean> {
|
||||
const res = await this.axiosInstance(`/delete/${DeleteHash}`, {
|
||||
method: 'GET',
|
||||
params: {
|
||||
hash: DeleteHash,
|
||||
format: 'json'
|
||||
}
|
||||
)
|
||||
})
|
||||
return res?.status === 200 && res?.data?.success
|
||||
}
|
||||
|
||||
@@ -184,7 +176,7 @@ class SmmsApi {
|
||||
* 上传文件
|
||||
* @param configMap
|
||||
*/
|
||||
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { fileArray } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
for (const item of fileArray) {
|
||||
@@ -221,7 +213,7 @@ class SmmsApi {
|
||||
* 下载文件
|
||||
* @param configMap
|
||||
*/
|
||||
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { downloadPath, fileArray, maxDownloadFileCount } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
const promises = [] as any
|
||||
@@ -239,19 +231,21 @@ class SmmsApi {
|
||||
sourceFileName: fileName,
|
||||
targetFilePath: savedFilePath
|
||||
})
|
||||
promises.push(() => new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger)
|
||||
.then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
promises.push(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger).then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
}
|
||||
const pool = new ConcurrencyPromisePool(maxDownloadFileCount)
|
||||
pool.all(promises).catch((error) => {
|
||||
pool.all(promises).catch(error => {
|
||||
this.logger.error(formatError(error, { class: 'SmmsApi', method: 'downloadBucketFile' }))
|
||||
})
|
||||
return true
|
||||
|
||||
+115
-70
@@ -1,4 +1,3 @@
|
||||
|
||||
import COS from 'cos-nodejs-sdk-v5'
|
||||
import { ipcMain, IpcMainEvent } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
@@ -18,7 +17,7 @@ class TcyunApi {
|
||||
ctx: COS
|
||||
logger: ManageLogger
|
||||
|
||||
constructor (secretId: string, secretKey: string, logger: ManageLogger) {
|
||||
constructor(secretId: string, secretKey: string, logger: ManageLogger) {
|
||||
this.ctx = new COS({
|
||||
SecretId: secretId,
|
||||
SecretKey: secretKey
|
||||
@@ -26,7 +25,7 @@ class TcyunApi {
|
||||
this.logger = logger
|
||||
}
|
||||
|
||||
formatFolder (item: {Prefix: string}, slicedPrefix: string, urlPrefix: string) {
|
||||
formatFolder(item: { Prefix: string }, slicedPrefix: string, urlPrefix: string) {
|
||||
return {
|
||||
...item,
|
||||
key: item.Prefix,
|
||||
@@ -41,7 +40,7 @@ class TcyunApi {
|
||||
}
|
||||
}
|
||||
|
||||
formatFile (item: COS.CosObject, slicedPrefix: string, urlPrefix: string): any {
|
||||
formatFile(item: COS.CosObject, slicedPrefix: string, urlPrefix: string): any {
|
||||
return {
|
||||
...item,
|
||||
key: item.Key,
|
||||
@@ -58,8 +57,8 @@ class TcyunApi {
|
||||
|
||||
/**
|
||||
* 获取存储桶列表
|
||||
*/
|
||||
async getBucketList (): Promise<any> {
|
||||
*/
|
||||
async getBucketList(): Promise<any> {
|
||||
const res = await this.ctx.getService({})
|
||||
return res?.Buckets || []
|
||||
}
|
||||
@@ -67,7 +66,7 @@ class TcyunApi {
|
||||
/**
|
||||
* 获取自定义域名
|
||||
*/
|
||||
async getBucketDomain (param: IStringKeyMap): Promise<any> {
|
||||
async getBucketDomain(param: IStringKeyMap): Promise<any> {
|
||||
const { bucketName, region } = param
|
||||
const res = await this.ctx.getBucketDomain({
|
||||
Bucket: bucketName,
|
||||
@@ -87,8 +86,8 @@ class TcyunApi {
|
||||
* }
|
||||
* @description
|
||||
* acl: private | publicRead | publicReadWrite
|
||||
*/
|
||||
async createBucket (configMap: IStringKeyMap): Promise < boolean > {
|
||||
*/
|
||||
async createBucket(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const res = await this.ctx.putBucket({
|
||||
ACL: configMap.acl,
|
||||
Bucket: configMap.BucketName,
|
||||
@@ -97,9 +96,15 @@ class TcyunApi {
|
||||
return res?.statusCode === 200
|
||||
}
|
||||
|
||||
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListRecursively(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, customUrl, cancelToken } = configMap
|
||||
const {
|
||||
bucketName: bucket,
|
||||
bucketConfig: { Location: region },
|
||||
prefix,
|
||||
customUrl,
|
||||
cancelToken
|
||||
} = configMap
|
||||
const slicedPrefix = prefix.slice(1, prefix.length)
|
||||
const urlPrefix = customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
|
||||
const cancelTask = [false]
|
||||
@@ -125,8 +130,11 @@ class TcyunApi {
|
||||
Marker: marker
|
||||
})
|
||||
if (res?.statusCode === 200) {
|
||||
result.fullList.push(...res.Contents.filter(item => parseInt(item.Size) !== 0)
|
||||
.map(item => this.formatFile(item, slicedPrefix, urlPrefix)))
|
||||
result.fullList.push(
|
||||
...res.Contents.filter(item => parseInt(item.Size) !== 0).map(item =>
|
||||
this.formatFile(item, slicedPrefix, urlPrefix)
|
||||
)
|
||||
)
|
||||
window.webContents.send(refreshDownloadFileTransferList, result)
|
||||
} else {
|
||||
result.finished = true
|
||||
@@ -142,9 +150,15 @@ class TcyunApi {
|
||||
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
|
||||
}
|
||||
|
||||
async getBucketListBackstage (configMap: IStringKeyMap): Promise < any > {
|
||||
async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, customUrl, cancelToken } = configMap
|
||||
const {
|
||||
bucketName: bucket,
|
||||
bucketConfig: { Location: region },
|
||||
prefix,
|
||||
customUrl,
|
||||
cancelToken
|
||||
} = configMap
|
||||
const slicedPrefix = prefix.slice(1, prefix.length)
|
||||
const urlPrefix = customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
|
||||
const cancelTask = [false]
|
||||
@@ -173,8 +187,9 @@ class TcyunApi {
|
||||
if (res?.statusCode === 200) {
|
||||
result.fullList.push(
|
||||
...res.CommonPrefixes.map(item => this.formatFolder(item, slicedPrefix, urlPrefix)),
|
||||
...res.Contents.filter(item => parseInt(item.Size) !== 0)
|
||||
.map(item => this.formatFile(item, slicedPrefix, urlPrefix))
|
||||
...res.Contents.filter(item => parseInt(item.Size) !== 0).map(item =>
|
||||
this.formatFile(item, slicedPrefix, urlPrefix)
|
||||
)
|
||||
)
|
||||
window.webContents.send('refreshFileTransferList', result)
|
||||
} else {
|
||||
@@ -205,19 +220,26 @@ class TcyunApi {
|
||||
* itemsPerPage: number,
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getBucketFileList (configMap: IStringKeyMap): Promise<any> {
|
||||
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, customUrl, marker, itemsPerPage } = configMap
|
||||
*/
|
||||
async getBucketFileList(configMap: IStringKeyMap): Promise<any> {
|
||||
const {
|
||||
bucketName: bucket,
|
||||
bucketConfig: { Location: region },
|
||||
prefix,
|
||||
customUrl,
|
||||
marker,
|
||||
itemsPerPage
|
||||
} = configMap
|
||||
const slicedPrefix = prefix.slice(1)
|
||||
const urlPrefix = customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
|
||||
const res = await this.ctx.getBucket({
|
||||
const res = (await this.ctx.getBucket({
|
||||
Bucket: bucket,
|
||||
Region: region,
|
||||
Prefix: slicedPrefix === '' ? undefined : slicedPrefix,
|
||||
Delimiter: '/',
|
||||
Marker: marker,
|
||||
MaxKeys: itemsPerPage
|
||||
}) as COS.GetBucketResult
|
||||
})) as COS.GetBucketResult
|
||||
if (res?.statusCode !== 200) {
|
||||
return {
|
||||
fullList: [],
|
||||
@@ -229,8 +251,9 @@ class TcyunApi {
|
||||
const result = {
|
||||
fullList: [
|
||||
...res.CommonPrefixes.map(item => this.formatFolder(item, slicedPrefix, urlPrefix)),
|
||||
...res.Contents.filter(item => parseInt(item.Size) !== 0)
|
||||
.map(item => this.formatFile(item, slicedPrefix, urlPrefix))
|
||||
...res.Contents.filter(item => parseInt(item.Size) !== 0).map(item =>
|
||||
this.formatFile(item, slicedPrefix, urlPrefix)
|
||||
)
|
||||
],
|
||||
isTruncated: res.IsTruncated === 'true',
|
||||
nextMarker: res.NextMarker || '',
|
||||
@@ -248,8 +271,8 @@ class TcyunApi {
|
||||
* oldKey: string,
|
||||
* newKey: string
|
||||
* }
|
||||
*/
|
||||
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
*/
|
||||
async renameBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, oldKey, newKey } = configMap
|
||||
const copyRes = await this.ctx.putObjectCopy({
|
||||
Bucket: bucketName,
|
||||
@@ -277,8 +300,8 @@ class TcyunApi {
|
||||
* region: string,
|
||||
* key: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
*/
|
||||
async deleteBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, key } = configMap
|
||||
const res = await this.ctx.deleteObject({
|
||||
Bucket: bucketName,
|
||||
@@ -292,7 +315,7 @@ class TcyunApi {
|
||||
* 删除文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async deleteBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, key } = configMap
|
||||
let marker
|
||||
let res: any
|
||||
@@ -317,7 +340,14 @@ class TcyunApi {
|
||||
marker = res.NextMarker
|
||||
} while (res.IsTruncated === 'true')
|
||||
for (const item of allFileList.CommonPrefixes) {
|
||||
if (!(await this.deleteBucketFolder({ bucketName, region, key: item.Prefix }))) return false
|
||||
if (
|
||||
!(await this.deleteBucketFolder({
|
||||
bucketName,
|
||||
region,
|
||||
key: item.Prefix
|
||||
}))
|
||||
)
|
||||
return false
|
||||
}
|
||||
const cycles = Math.ceil(allFileList.Contents.length / 1000)
|
||||
for (let i = 0; i < cycles; i++) {
|
||||
@@ -342,16 +372,18 @@ class TcyunApi {
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
|
||||
async getPreSignedUrl(configMap: IStringKeyMap): Promise<string> {
|
||||
const { bucketName, region, key, expires, customUrl } = configMap
|
||||
const res = this.ctx.getObjectUrl({
|
||||
Bucket: bucketName,
|
||||
Region: region,
|
||||
Key: key,
|
||||
Expires: expires,
|
||||
Sign: true
|
||||
}, () => {
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
@@ -359,7 +391,7 @@ class TcyunApi {
|
||||
* 高级上传文件
|
||||
* @param configMap
|
||||
*/
|
||||
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { fileArray } = configMap
|
||||
// fileArray = [{
|
||||
// bucketName: string,
|
||||
@@ -412,7 +444,12 @@ class TcyunApi {
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
} else {
|
||||
this.logger.error(formatError(err, { method: 'uploadBucketFile', class: 'TcyunApi' }))
|
||||
this.logger.error(
|
||||
formatError(err, {
|
||||
method: 'uploadBucketFile',
|
||||
class: 'TcyunApi'
|
||||
})
|
||||
)
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 0,
|
||||
@@ -434,7 +471,7 @@ class TcyunApi {
|
||||
* 新建文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async createBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { bucketName, region, key } = configMap
|
||||
const res = await this.ctx.putObject({
|
||||
Bucket: bucketName,
|
||||
@@ -449,7 +486,7 @@ class TcyunApi {
|
||||
* 下载文件
|
||||
* @param configMap
|
||||
*/
|
||||
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { downloadPath, fileArray } = configMap
|
||||
// fileArray = [{
|
||||
// bucketName: string,
|
||||
@@ -472,38 +509,46 @@ class TcyunApi {
|
||||
targetFilePath: path.join(downloadPath, fileName)
|
||||
})
|
||||
fs.ensureDirSync(path.dirname(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) => {
|
||||
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: Math.floor(progress.percent * 100),
|
||||
status: downloadTaskSpecialStatus.downloading
|
||||
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()
|
||||
})
|
||||
}
|
||||
}).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()
|
||||
.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
|
||||
}
|
||||
|
||||
@@ -7,7 +7,15 @@ import Upyun from 'upyun'
|
||||
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
|
||||
import { md5, hmacSha1Base64, getFileMimeType, NewDownloader, gotUpload, ConcurrencyPromisePool, formatError } from '~/manage/utils/common'
|
||||
import {
|
||||
md5,
|
||||
hmacSha1Base64,
|
||||
getFileMimeType,
|
||||
NewDownloader,
|
||||
gotUpload,
|
||||
ConcurrencyPromisePool,
|
||||
formatError
|
||||
} from '~/manage/utils/common'
|
||||
import { ManageLogger } from '~/manage/utils/logger'
|
||||
import UpDownTaskQueue from '~/manage/datastore/upDownTaskQueue'
|
||||
|
||||
@@ -26,7 +34,14 @@ class UpyunApi {
|
||||
stopMarker = 'g2gCZAAEbmV4dGQAA2VvZg'
|
||||
logger: ManageLogger
|
||||
|
||||
constructor (bucket: string, operator: string, password: string, logger: ManageLogger, antiLeechToken?: string, expireTime?: number) {
|
||||
constructor(
|
||||
bucket: string,
|
||||
operator: string,
|
||||
password: string,
|
||||
logger: ManageLogger,
|
||||
antiLeechToken?: string,
|
||||
expireTime?: number
|
||||
) {
|
||||
this.ser = new Upyun.Service(bucket, operator, password)
|
||||
this.cli = new Upyun.Client(this.ser)
|
||||
this.bucket = bucket
|
||||
@@ -37,7 +52,7 @@ class UpyunApi {
|
||||
this.expireTime = expireTime || 24 * 60 * 60
|
||||
}
|
||||
|
||||
getAntiLeechParam (key: string): string {
|
||||
getAntiLeechParam(key: string): string {
|
||||
const uri = `/${key}`.replace(/%2F/g, '/').replace(/^\/+/g, '/')
|
||||
const now = Math.round(new Date().getTime() / 1000)
|
||||
const expire = this.expireTime ? now + parseInt(this.expireTime.toString(), 10) : now + 1800
|
||||
@@ -46,7 +61,7 @@ class UpyunApi {
|
||||
return `_upt=${upt}`
|
||||
}
|
||||
|
||||
formatFolder (item: any, slicedPrefix: string, urlPrefix: string) {
|
||||
formatFolder(item: any, slicedPrefix: string, urlPrefix: string) {
|
||||
const key = `${slicedPrefix}${item.name}/`
|
||||
let url = `${urlPrefix}/${key}`
|
||||
if (this.antiLeechToken) {
|
||||
@@ -67,7 +82,7 @@ class UpyunApi {
|
||||
}
|
||||
}
|
||||
|
||||
formatFile (item: any, slicedPrefix: string, urlPrefix: string) {
|
||||
formatFile(item: any, slicedPrefix: string, urlPrefix: string) {
|
||||
const key = `${slicedPrefix}${item.name}`
|
||||
let url = `${urlPrefix}/${key}`
|
||||
if (this.antiLeechToken) {
|
||||
@@ -87,13 +102,7 @@ class UpyunApi {
|
||||
}
|
||||
}
|
||||
|
||||
authorization (
|
||||
method: string,
|
||||
uri: string,
|
||||
contentMd5: string,
|
||||
operator: string,
|
||||
password: string
|
||||
) {
|
||||
authorization(method: string, uri: string, contentMd5: string, operator: string, password: string) {
|
||||
return `UPYUN ${operator}:${hmacSha1Base64(
|
||||
md5(password, 'hex'),
|
||||
`${method.toUpperCase()}&${encodeURI(uri)}&${new Date().toUTCString()}${contentMd5 ? `&${contentMd5}` : ''}`
|
||||
@@ -103,11 +112,11 @@ class UpyunApi {
|
||||
/**
|
||||
* 获取空间列表
|
||||
*/
|
||||
async getBucketList (): Promise<any> {
|
||||
async getBucketList(): Promise<any> {
|
||||
return this.bucket
|
||||
}
|
||||
|
||||
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListRecursively(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: bucket, prefix, cancelToken } = configMap
|
||||
const slicedPrefix = prefix.slice(1)
|
||||
@@ -159,7 +168,7 @@ class UpyunApi {
|
||||
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
|
||||
}
|
||||
|
||||
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { bucketName: bucket, prefix, cancelToken } = configMap
|
||||
const slicedPrefix = prefix.slice(1)
|
||||
@@ -217,8 +226,8 @@ class UpyunApi {
|
||||
* itemsPerPage: number,
|
||||
* customUrl: string
|
||||
* }
|
||||
*/
|
||||
async getBucketFileList (configMap: IStringKeyMap): Promise<any> {
|
||||
*/
|
||||
async getBucketFileList(configMap: IStringKeyMap): Promise<any> {
|
||||
const { bucketName: bucket, prefix, marker, itemsPerPage } = configMap
|
||||
const slicedPrefix = prefix.slice(1)
|
||||
const urlPrefix = configMap.customUrl || `http://${bucket}.test.upcdn.net`
|
||||
@@ -246,16 +255,16 @@ class UpyunApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* oldKey: string,
|
||||
* newKey: string
|
||||
* }
|
||||
*/
|
||||
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 重命名文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* oldKey: string,
|
||||
* newKey: string
|
||||
* }
|
||||
*/
|
||||
async renameBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const oldKey = configMap.oldKey
|
||||
let newKey = configMap.newKey
|
||||
const method = 'PUT'
|
||||
@@ -280,25 +289,25 @@ class UpyunApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* 删除文件
|
||||
* @param configMap
|
||||
* configMap = {
|
||||
* bucketName: string,
|
||||
* region: string,
|
||||
* key: string
|
||||
* }
|
||||
*/
|
||||
async deleteBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
const res = await this.cli.deleteFile(key)
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* delete bucket folder
|
||||
* @param configMap
|
||||
*/
|
||||
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
* delete bucket folder
|
||||
* @param configMap
|
||||
*/
|
||||
async deleteBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
let marker = ''
|
||||
let isTruncated
|
||||
@@ -313,14 +322,16 @@ class UpyunApi {
|
||||
})
|
||||
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}/`
|
||||
})
|
||||
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
|
||||
@@ -360,7 +371,7 @@ class UpyunApi {
|
||||
* axiso:onUploadProgress not work in nodejs , use got instead
|
||||
* @param configMap
|
||||
*/
|
||||
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { fileArray } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
fileArray.forEach((item: any) => {
|
||||
@@ -416,7 +427,7 @@ class UpyunApi {
|
||||
* 新建文件夹
|
||||
* @param configMap
|
||||
*/
|
||||
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async createBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
const res = await this.cli.makeDir(`/${key}`)
|
||||
return res
|
||||
@@ -426,7 +437,7 @@ class UpyunApi {
|
||||
* 下载文件
|
||||
* @param configMap
|
||||
*/
|
||||
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { downloadPath, fileArray, maxDownloadFileCount } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
const promises = [] as any
|
||||
@@ -445,19 +456,21 @@ class UpyunApi {
|
||||
targetFilePath: savedFilePath
|
||||
})
|
||||
const preSignedUrl = `${customUrl}/${key}`
|
||||
promises.push(() => new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger)
|
||||
.then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
promises.push(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger).then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
}
|
||||
const pool = new ConcurrencyPromisePool(maxDownloadFileCount)
|
||||
pool.all(promises).catch((error) => {
|
||||
pool.all(promises).catch(error => {
|
||||
this.logger.error(formatError(error, { class: 'UpyunApi', method: 'downloadBucketFile' }))
|
||||
})
|
||||
return true
|
||||
|
||||
@@ -29,7 +29,15 @@ class WebdavplistApi {
|
||||
agent: https.Agent | http.Agent
|
||||
ctx: WebDAVClient
|
||||
|
||||
constructor (endpoint: string, username: string, password: string, sslEnabled: boolean, proxy: string | undefined, authType: 'basic' | 'digest' | undefined, logger: ManageLogger) {
|
||||
constructor(
|
||||
endpoint: string,
|
||||
username: string,
|
||||
password: string,
|
||||
sslEnabled: boolean,
|
||||
proxy: string | undefined,
|
||||
authType: 'basic' | 'digest' | undefined,
|
||||
logger: ManageLogger
|
||||
) {
|
||||
this.endpoint = formatEndpoint(endpoint, sslEnabled)
|
||||
this.username = username
|
||||
this.password = password
|
||||
@@ -50,16 +58,12 @@ class WebdavplistApi {
|
||||
if (this.authType === 'digest') {
|
||||
options.authType = AuthType.Digest
|
||||
}
|
||||
this.ctx = createClient(
|
||||
this.endpoint,
|
||||
options
|
||||
)
|
||||
this.ctx = createClient(this.endpoint, options)
|
||||
}
|
||||
|
||||
logParam = (error:any, method: string) =>
|
||||
this.logger.error(formatError(error, { class: 'WebdavplistApi', method }))
|
||||
logParam = (error: any, method: string) => this.logger.error(formatError(error, { class: 'WebdavplistApi', method }))
|
||||
|
||||
formatFolder (item: FileStat, urlPrefix: string, isWebPath = false) {
|
||||
formatFolder(item: FileStat, urlPrefix: string, isWebPath = false) {
|
||||
const key = item.filename.replace(/^\/+/, '')
|
||||
return {
|
||||
...item,
|
||||
@@ -76,7 +80,7 @@ class WebdavplistApi {
|
||||
}
|
||||
}
|
||||
|
||||
formatFile (item: FileStat, urlPrefix: string, isWebPath = false) {
|
||||
formatFile(item: FileStat, urlPrefix: string, isWebPath = false) {
|
||||
const key = item.filename.replace(/^\/+/, '')
|
||||
return {
|
||||
...item,
|
||||
@@ -95,7 +99,7 @@ class WebdavplistApi {
|
||||
|
||||
isRequestSuccess = (code: number) => code >= 200 && code < 300
|
||||
|
||||
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListRecursively(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { prefix, customUrl, cancelToken } = configMap
|
||||
const urlPrefix = customUrl || this.endpoint
|
||||
@@ -135,7 +139,7 @@ class WebdavplistApi {
|
||||
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
|
||||
}
|
||||
|
||||
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
|
||||
async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
|
||||
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
const { prefix, customUrl, cancelToken, baseDir } = configMap
|
||||
let urlPrefix = customUrl || this.endpoint
|
||||
@@ -166,7 +170,8 @@ class WebdavplistApi {
|
||||
if (res.data?.length) {
|
||||
res.data.forEach((item: FileStat) => {
|
||||
const relativePath = path.relative(baseDir, item.filename)
|
||||
const relative = webPath && urlPrefix + `/${path.join(webPath, relativePath)}`.replace(/\\/g, '/').replace(/\/+/g, '/')
|
||||
const relative =
|
||||
webPath && urlPrefix + `/${path.join(webPath, relativePath)}`.replace(/\\/g, '/').replace(/\/+/g, '/')
|
||||
if (item.type === 'directory') {
|
||||
result.fullList.push(this.formatFolder(item, webPath ? relative : urlPrefix, !!webPath))
|
||||
} else {
|
||||
@@ -193,7 +198,7 @@ class WebdavplistApi {
|
||||
ipcMain.removeAllListeners('cancelLoadingFileList')
|
||||
}
|
||||
|
||||
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async renameBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { oldKey, newKey } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -205,7 +210,7 @@ class WebdavplistApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async deleteBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -217,7 +222,7 @@ class WebdavplistApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async deleteBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -229,7 +234,7 @@ class WebdavplistApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
|
||||
async getPreSignedUrl(configMap: IStringKeyMap): Promise<string> {
|
||||
const { key } = configMap
|
||||
let result = ''
|
||||
try {
|
||||
@@ -241,7 +246,7 @@ class WebdavplistApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { fileArray } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
for (const item of fileArray) {
|
||||
@@ -261,10 +266,8 @@ class WebdavplistApi {
|
||||
targetFileRegion: region,
|
||||
noProgress: true
|
||||
})
|
||||
this.ctx.putFileContents(
|
||||
key,
|
||||
this.authType === 'digest' ? fs.readFileSync(filePath) : fs.createReadStream(filePath),
|
||||
{
|
||||
this.ctx
|
||||
.putFileContents(key, this.authType === 'digest' ? fs.readFileSync(filePath) : fs.createReadStream(filePath), {
|
||||
overwrite: true,
|
||||
onUploadProgress: (progressEvent: ProgressEvent) => {
|
||||
instance.updateUploadTask({
|
||||
@@ -273,37 +276,38 @@ class WebdavplistApi {
|
||||
status: uploadTaskSpecialStatus.uploading
|
||||
})
|
||||
}
|
||||
}
|
||||
).then((res: boolean) => {
|
||||
if (res) {
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 100,
|
||||
status: uploadTaskSpecialStatus.uploaded,
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
} else {
|
||||
})
|
||||
.then((res: boolean) => {
|
||||
if (res) {
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 100,
|
||||
status: uploadTaskSpecialStatus.uploaded,
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
} else {
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 0,
|
||||
status: commonTaskStatus.failed,
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.logParam(error, 'uploadBucketFile')
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 0,
|
||||
status: commonTaskStatus.failed,
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
this.logParam(error, 'uploadBucketFile')
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: 0,
|
||||
status: commonTaskStatus.failed,
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async createBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { key } = configMap
|
||||
let result = false
|
||||
try {
|
||||
@@ -317,7 +321,7 @@ class WebdavplistApi {
|
||||
return result
|
||||
}
|
||||
|
||||
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
|
||||
async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
|
||||
const { downloadPath, fileArray, maxDownloadFileCount } = configMap
|
||||
const instance = UpDownTaskQueue.getInstance()
|
||||
const promises = [] as any
|
||||
@@ -345,25 +349,35 @@ class WebdavplistApi {
|
||||
Authorization: `Basic ${base64Str}`
|
||||
}
|
||||
} else if (this.authType === 'digest') {
|
||||
const authHeader = await getAuthHeader('GET', this.endpoint, `/${key.replace(/^\/+/, '')}`, this.username, this.password)
|
||||
const authHeader = await getAuthHeader(
|
||||
'GET',
|
||||
this.endpoint,
|
||||
`/${key.replace(/^\/+/, '')}`,
|
||||
this.username,
|
||||
this.password
|
||||
)
|
||||
headers = {
|
||||
Authorization: authHeader
|
||||
}
|
||||
preSignedUrl = `${this.endpoint}/${key.replace(/^\/+/, '')}`
|
||||
}
|
||||
promises.push(() => new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger, this.proxyStr, headers)
|
||||
.then((res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
promises.push(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger, this.proxyStr, headers).then(
|
||||
(res: boolean) => {
|
||||
if (res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}))
|
||||
)
|
||||
}
|
||||
const pool = new ConcurrencyPromisePool(maxDownloadFileCount)
|
||||
pool.all(promises).catch((error) => {
|
||||
pool.all(promises).catch(error => {
|
||||
this.logParam(error, 'downloadBucketFile')
|
||||
})
|
||||
return true
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IManageApiType, IManageConfigType } from '#/types/manage'
|
||||
class ManageDB {
|
||||
readonly #ctx: IManageApiType
|
||||
readonly #db: JSONStore
|
||||
constructor (ctx: IManageApiType) {
|
||||
constructor(ctx: IManageApiType) {
|
||||
this.#ctx = ctx
|
||||
this.#db = new JSONStore(this.#ctx.configPath)
|
||||
const initParams: IStringKeyMap = {
|
||||
@@ -25,37 +25,37 @@ class ManageDB {
|
||||
}
|
||||
}
|
||||
|
||||
read (flush?: boolean): IJSON {
|
||||
read(flush?: boolean): IJSON {
|
||||
return this.#db.read(flush)
|
||||
}
|
||||
|
||||
get (key: string = ''): any {
|
||||
get(key: string = ''): any {
|
||||
this.read(true)
|
||||
return this.#db.get(key)
|
||||
}
|
||||
|
||||
set (key: string, value: any): void {
|
||||
set(key: string, value: any): void {
|
||||
this.read(true)
|
||||
return this.#db.set(key, value)
|
||||
}
|
||||
|
||||
has (key: string): boolean {
|
||||
has(key: string): boolean {
|
||||
this.read(true)
|
||||
return this.#db.has(key)
|
||||
}
|
||||
|
||||
unset (key: string, value: any): boolean {
|
||||
unset(key: string, value: any): boolean {
|
||||
this.read(true)
|
||||
return this.#db.unset(key, value)
|
||||
}
|
||||
|
||||
saveConfig (config: Partial<IManageConfigType>): void {
|
||||
saveConfig(config: Partial<IManageConfigType>): void {
|
||||
Object.keys(config).forEach((name: string) => {
|
||||
this.set(name, config[name])
|
||||
})
|
||||
}
|
||||
|
||||
removeConfig (config: IManageConfigType): void {
|
||||
removeConfig(config: IManageConfigType): void {
|
||||
Object.keys(config).forEach((name: string) => {
|
||||
this.unset(name, config[name])
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ const errorMsg = {
|
||||
/** ensure notification list */
|
||||
if (!global.notificationList) global.notificationList = []
|
||||
|
||||
function manageDbChecker () {
|
||||
function manageDbChecker() {
|
||||
if (process.type !== 'renderer') {
|
||||
const manageConfigFilePath = managePathChecker()
|
||||
if (!fs.existsSync(manageConfigFilePath)) {
|
||||
@@ -42,9 +42,13 @@ function manageDbChecker () {
|
||||
fs.unlinkSync(manageConfigFilePath)
|
||||
if (fs.existsSync(manageConfigFileBackupPath)) {
|
||||
try {
|
||||
configFile = fs.readFileSync(manageConfigFileBackupPath, { encoding: 'utf-8' })
|
||||
configFile = fs.readFileSync(manageConfigFileBackupPath, {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
JSON.parse(configFile)
|
||||
writeFile.sync(manageConfigFilePath, configFile, { encoding: 'utf-8' })
|
||||
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')
|
||||
@@ -61,14 +65,16 @@ function manageDbChecker () {
|
||||
global.notificationList.push(optionsTpl)
|
||||
return
|
||||
}
|
||||
writeFile.sync(manageConfigFileBackupPath, configFile, { encoding: 'utf-8' })
|
||||
writeFile.sync(manageConfigFileBackupPath, configFile, {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get manage config path
|
||||
*/
|
||||
function managePathChecker (): string {
|
||||
function managePathChecker(): string {
|
||||
if (_configFilePath) {
|
||||
return _configFilePath
|
||||
}
|
||||
@@ -80,7 +86,9 @@ function managePathChecker (): string {
|
||||
return _configFilePath
|
||||
}
|
||||
try {
|
||||
const configString = fs.readFileSync(defaultManageConfigPath, { encoding: 'utf-8' })
|
||||
const configString = fs.readFileSync(defaultManageConfigPath, {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
const config = JSON.parse(configString)
|
||||
const userConfigPath: string = config.configPath || ''
|
||||
if (userConfigPath) {
|
||||
@@ -107,12 +115,8 @@ function managePathChecker (): string {
|
||||
}
|
||||
}
|
||||
|
||||
function managePathDir () {
|
||||
function managePathDir() {
|
||||
return path.dirname(managePathChecker())
|
||||
}
|
||||
|
||||
export {
|
||||
managePathChecker,
|
||||
managePathDir,
|
||||
manageDbChecker
|
||||
}
|
||||
export { managePathChecker, managePathDir, manageDbChecker }
|
||||
|
||||
@@ -9,8 +9,8 @@ import { commonTaskStatus, downloadTaskSpecialStatus, uploadTaskSpecialStatus }
|
||||
import { IDownloadTask, IUploadTask } from '#/types/manage'
|
||||
|
||||
class UpDownTaskQueue {
|
||||
/* eslint-disable */
|
||||
private static instance: UpDownTaskQueue
|
||||
/* eslint-disable */
|
||||
private static instance: UpDownTaskQueue
|
||||
/* eslint-enable */
|
||||
private uploadTaskQueue = <IUploadTask[]>[]
|
||||
|
||||
@@ -18,38 +18,38 @@ class UpDownTaskQueue {
|
||||
|
||||
private persistPath = path.join(app.getPath('userData'), 'UpDownTaskQueue.json')
|
||||
|
||||
private constructor () {
|
||||
private constructor() {
|
||||
this.restore()
|
||||
}
|
||||
|
||||
static getInstance () {
|
||||
static getInstance() {
|
||||
if (!UpDownTaskQueue.instance) {
|
||||
UpDownTaskQueue.instance = new UpDownTaskQueue()
|
||||
}
|
||||
return UpDownTaskQueue.instance
|
||||
}
|
||||
|
||||
getUploadTaskQueue () {
|
||||
getUploadTaskQueue() {
|
||||
return UpDownTaskQueue.getInstance().uploadTaskQueue
|
||||
}
|
||||
|
||||
getDownloadTaskQueue () {
|
||||
getDownloadTaskQueue() {
|
||||
return UpDownTaskQueue.getInstance().downloadTaskQueue
|
||||
}
|
||||
|
||||
getUploadTask (taskId: string) {
|
||||
getUploadTask(taskId: string) {
|
||||
return UpDownTaskQueue.getInstance().uploadTaskQueue.find(item => item.id === taskId)
|
||||
}
|
||||
|
||||
getAllUploadTask () {
|
||||
getAllUploadTask() {
|
||||
return UpDownTaskQueue.getInstance().uploadTaskQueue
|
||||
}
|
||||
|
||||
addUploadTask (task: IUploadTask) {
|
||||
addUploadTask(task: IUploadTask) {
|
||||
UpDownTaskQueue.getInstance().uploadTaskQueue.push(task)
|
||||
}
|
||||
|
||||
updateUploadTask (task: Partial<IUploadTask>) {
|
||||
updateUploadTask(task: Partial<IUploadTask>) {
|
||||
const taskIndex = UpDownTaskQueue.getInstance().uploadTaskQueue.findIndex(item => item.id === task.id)
|
||||
if (taskIndex !== -1) {
|
||||
const taskKeys = Object.keys(task)
|
||||
@@ -61,33 +61,33 @@ class UpDownTaskQueue {
|
||||
}
|
||||
}
|
||||
|
||||
removeUploadTask (taskId: string) {
|
||||
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) {
|
||||
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) {
|
||||
getDownloadTask(taskId: string) {
|
||||
return UpDownTaskQueue.getInstance().downloadTaskQueue.find(item => item.id === taskId)
|
||||
}
|
||||
|
||||
getAllDownloadTask () {
|
||||
getAllDownloadTask() {
|
||||
return UpDownTaskQueue.getInstance().downloadTaskQueue
|
||||
}
|
||||
|
||||
addDownloadTask (task: IDownloadTask) {
|
||||
addDownloadTask(task: IDownloadTask) {
|
||||
UpDownTaskQueue.getInstance().downloadTaskQueue.push(task)
|
||||
}
|
||||
|
||||
updateDownloadTask (task: Partial<IDownloadTask>) {
|
||||
updateDownloadTask(task: Partial<IDownloadTask>) {
|
||||
const taskIndex = UpDownTaskQueue.getInstance().downloadTaskQueue.findIndex(item => item.id === task.id)
|
||||
if (taskIndex !== -1) {
|
||||
const taskKeys = Object.keys(task)
|
||||
@@ -99,40 +99,53 @@ class UpDownTaskQueue {
|
||||
}
|
||||
}
|
||||
|
||||
clearUploadTaskQueue () {
|
||||
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)
|
||||
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)
|
||||
removeDownloadedTask() {
|
||||
UpDownTaskQueue.getInstance().downloadTaskQueue = UpDownTaskQueue.getInstance().downloadTaskQueue.filter(
|
||||
item =>
|
||||
item.status !== downloadTaskSpecialStatus.downloaded &&
|
||||
item.status !== commonTaskStatus.canceled &&
|
||||
item.status !== commonTaskStatus.failed
|
||||
)
|
||||
}
|
||||
|
||||
clearDownloadTaskQueue () {
|
||||
clearDownloadTaskQueue() {
|
||||
UpDownTaskQueue.getInstance().downloadTaskQueue = []
|
||||
}
|
||||
|
||||
clearAllTaskQueue () {
|
||||
clearAllTaskQueue() {
|
||||
this.clearUploadTaskQueue()
|
||||
this.clearDownloadTaskQueue()
|
||||
}
|
||||
|
||||
persist () {
|
||||
persist() {
|
||||
try {
|
||||
this.checkPersistPath()
|
||||
fs.writeFileSync(this.persistPath, JSON.stringify({
|
||||
uploadTaskQueue: this.uploadTaskQueue,
|
||||
downloadTaskQueue: this.downloadTaskQueue
|
||||
}))
|
||||
fs.writeFileSync(
|
||||
this.persistPath,
|
||||
JSON.stringify({
|
||||
uploadTaskQueue: this.uploadTaskQueue,
|
||||
downloadTaskQueue: this.downloadTaskQueue
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
private restore () {
|
||||
private restore() {
|
||||
try {
|
||||
this.checkPersistPath()
|
||||
const persistData = JSON.parse(fs.readFileSync(this.persistPath, { encoding: 'utf-8' }))
|
||||
@@ -144,20 +157,26 @@ class UpDownTaskQueue {
|
||||
}
|
||||
}
|
||||
|
||||
private checkPersistPath () {
|
||||
private checkPersistPath() {
|
||||
if (!fs.existsSync(this.persistPath)) {
|
||||
fs.writeFileSync(this.persistPath, JSON.stringify({
|
||||
uploadTaskQueue: this.uploadTaskQueue,
|
||||
downloadTaskQueue: this.downloadTaskQueue
|
||||
}))
|
||||
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
|
||||
}))
|
||||
fs.writeFileSync(
|
||||
this.persistPath,
|
||||
JSON.stringify({
|
||||
uploadTaskQueue: this.uploadTaskQueue,
|
||||
downloadTaskQueue: this.downloadTaskQueue
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const PICLIST_MANAGE_GET_CONFIG = 'PICLIST_MANAGE_GET_CONFIG'
|
||||
export const PICLIST_MANAGE_REMOVE_CONFIG = 'PICLIST_MANAGE_REMOVE_CONFIG'
|
||||
export const PICLIST_MANAGE_SAVE_CONFIG = 'PICLIST_MANAGE_SAVE_CONFIG'
|
||||
@@ -1,155 +0,0 @@
|
||||
import { ipcMain, IpcMainInvokeEvent, dialog, app, shell } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
|
||||
import UpDownTaskQueue from '~/manage/datastore/upDownTaskQueue'
|
||||
import manageCoreIPC from '~/manage/events/manageCoreIPC'
|
||||
import { ManageApi } from '~/manage/manageApi'
|
||||
import { downloadFileFromUrl } from '~/manage/utils/common'
|
||||
|
||||
import { selectDownloadFolder } from '#/utils/static'
|
||||
|
||||
export const manageIpcList = {
|
||||
listen () {
|
||||
manageCoreIPC.listen()
|
||||
|
||||
ipcMain.handle('getBucketList', async (_: IpcMainInvokeEvent, currentPicBed: string) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
return manage.getBucketList()
|
||||
})
|
||||
|
||||
ipcMain.handle('createBucket', async (_: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
return manage.createBucket(param)
|
||||
})
|
||||
|
||||
ipcMain.handle('getBucketFileList', async (_: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
return manage.getBucketFileList(param)
|
||||
})
|
||||
|
||||
ipcMain.handle('getBucketDomain', async (_: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
const result = await manage.getBucketDomain(param)
|
||||
return result
|
||||
})
|
||||
|
||||
ipcMain.handle('setBucketAclPolicy', async (_: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
return manage.setBucketAclPolicy(param)
|
||||
})
|
||||
|
||||
ipcMain.handle('renameBucketFile', async (_: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
return manage.renameBucketFile(param)
|
||||
})
|
||||
|
||||
ipcMain.handle('deleteBucketFile', async (_: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
return manage.deleteBucketFile(param)
|
||||
})
|
||||
|
||||
ipcMain.handle('deleteBucketFolder', async (_: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
return manage.deleteBucketFolder(param)
|
||||
})
|
||||
|
||||
ipcMain.on('getBucketListBackstage', async (_: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
return manage.getBucketListBackstage(param)
|
||||
})
|
||||
|
||||
ipcMain.on('getBucketListRecursively', async (_: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
return manage.getBucketListRecursively(param)
|
||||
})
|
||||
|
||||
ipcMain.handle('convertPathToBase64', async (_: IpcMainInvokeEvent, filePath: string) => {
|
||||
const res = fs.readFileSync(filePath, 'base64')
|
||||
return res
|
||||
})
|
||||
|
||||
ipcMain.handle('openFileSelectDialog', async () => {
|
||||
const res = await dialog.showOpenDialog({
|
||||
properties: ['openFile', 'multiSelections']
|
||||
})
|
||||
if (res.canceled) {
|
||||
return []
|
||||
} else {
|
||||
return res.filePaths
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('getPreSignedUrl', async (_: 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 (_: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
return manage.uploadBucketFile(param)
|
||||
})
|
||||
|
||||
ipcMain.on('downloadBucketFile', async (_: IpcMainInvokeEvent, currentPicBed: string, param: IStringKeyMap) => {
|
||||
const manage = new ManageApi(currentPicBed)
|
||||
return manage.downloadBucketFile(param)
|
||||
})
|
||||
|
||||
ipcMain.handle('createBucketFolder', async (_: 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 (_: IpcMainInvokeEvent, path: string | undefined) => {
|
||||
if (path) {
|
||||
shell.showItemInFolder(path)
|
||||
} else {
|
||||
shell.openPath(app.getPath('downloads'))
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('OpenLocalFile', async (_: IpcMainInvokeEvent, fullPath: string) => {
|
||||
fs.existsSync(fullPath) ? shell.showItemInFolder(fullPath) : shell.openPath(path.dirname(fullPath))
|
||||
})
|
||||
|
||||
ipcMain.handle('downloadFileFromUrl', async (_: IpcMainInvokeEvent, urls: string[]) => {
|
||||
const res = await downloadFileFromUrl(urls)
|
||||
return res
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import {
|
||||
IpcMainEvent,
|
||||
IpcMainInvokeEvent,
|
||||
ipcMain
|
||||
} from 'electron'
|
||||
|
||||
import getManageApi from '~/manage/Main'
|
||||
import { PICLIST_MANAGE_GET_CONFIG, PICLIST_MANAGE_SAVE_CONFIG, PICLIST_MANAGE_REMOVE_CONFIG } from '~/manage/events/constants'
|
||||
|
||||
const manageApi = getManageApi()
|
||||
|
||||
const handleManageGetConfig = () => {
|
||||
ipcMain.handle(PICLIST_MANAGE_GET_CONFIG, (_: IpcMainInvokeEvent, key: string | undefined) => {
|
||||
return manageApi.getConfig(key)
|
||||
})
|
||||
}
|
||||
|
||||
const handleManageSaveConfig = () => {
|
||||
ipcMain.on(PICLIST_MANAGE_SAVE_CONFIG, (_: IpcMainEvent, data: any) => {
|
||||
manageApi.saveConfig(data)
|
||||
})
|
||||
}
|
||||
|
||||
const handleManageRemoveConfig = () => {
|
||||
ipcMain.on(PICLIST_MANAGE_REMOVE_CONFIG, (_: IpcMainEvent, key: string, propName: string) => {
|
||||
manageApi.removeConfig(key, propName)
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
listen () {
|
||||
handleManageGetConfig()
|
||||
handleManageSaveConfig()
|
||||
handleManageRemoveConfig()
|
||||
}
|
||||
}
|
||||
+114
-113
@@ -1,4 +1,3 @@
|
||||
|
||||
import { ipcMain } from 'electron'
|
||||
import { EventEmitter } from 'events'
|
||||
import fs from 'fs-extra'
|
||||
@@ -15,12 +14,7 @@ import { isInputConfigValid, formatError } from '~/manage/utils/common'
|
||||
import { ManageLogger } from '~/manage/utils/logger'
|
||||
|
||||
import { IWindowList } from '#/types/enum'
|
||||
import {
|
||||
IManageApiType,
|
||||
IManageConfigType,
|
||||
IManageError,
|
||||
IPicBedMangeConfig
|
||||
} from '#/types/manage'
|
||||
import { IManageApiType, IManageConfigType, IManageError, IPicBedMangeConfig } from '#/types/manage'
|
||||
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '#/utils/static'
|
||||
|
||||
export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
@@ -32,7 +26,7 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
logger: ManageLogger
|
||||
currentPicBedConfig: IPicBedMangeConfig
|
||||
|
||||
constructor (currentPicBed: string = '') {
|
||||
constructor(currentPicBed: string = '') {
|
||||
super()
|
||||
this.currentPicBed = currentPicBed || 'placeholder'
|
||||
this.configPath = managePathChecker()
|
||||
@@ -42,7 +36,7 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
this.currentPicBedConfig = this.getPicBedConfig(this.currentPicBed)
|
||||
}
|
||||
|
||||
getMsgParam (method: string) {
|
||||
getMsgParam(method: string) {
|
||||
return {
|
||||
class: 'ManageApi',
|
||||
method,
|
||||
@@ -50,19 +44,33 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
errorMsg (err: any, param: IStringKeyMap) {
|
||||
errorMsg(err: any, param: IStringKeyMap) {
|
||||
this.logger.error(formatError(err, param))
|
||||
}
|
||||
|
||||
createClient () {
|
||||
createClient() {
|
||||
const name = this.currentPicBedConfig.picBedName
|
||||
switch (name) {
|
||||
case 'aliyun':
|
||||
return new API.AliyunApi(this.currentPicBedConfig.accessKeyId, this.currentPicBedConfig.accessKeySecret, this.logger)
|
||||
return new API.AliyunApi(
|
||||
this.currentPicBedConfig.accessKeyId,
|
||||
this.currentPicBedConfig.accessKeySecret,
|
||||
this.logger
|
||||
)
|
||||
case 'github':
|
||||
return new API.GithubApi(this.currentPicBedConfig.token, this.currentPicBedConfig.githubUsername, this.currentPicBedConfig.proxy, this.logger)
|
||||
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)
|
||||
return new API.ImgurApi(
|
||||
this.currentPicBedConfig.imgurUserName,
|
||||
this.currentPicBedConfig.accessToken,
|
||||
this.currentPicBedConfig.proxy,
|
||||
this.logger
|
||||
)
|
||||
case 'local':
|
||||
return new API.LocalApi(this.logger)
|
||||
case 'qiniu':
|
||||
@@ -70,25 +78,60 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
case 'smms':
|
||||
return new API.SmmsApi(this.currentPicBedConfig.token, this.logger)
|
||||
case 's3plist':
|
||||
return new API.S3plistApi(this.currentPicBedConfig.accessKeyId, this.currentPicBedConfig.secretAccessKey, this.currentPicBedConfig.endpoint, this.currentPicBedConfig.sslEnabled, this.currentPicBedConfig.s3ForcePathStyle, this.currentPicBedConfig.proxy, this.logger, this.currentPicBedConfig.dogeCloudSupport || false, this.currentPicBedConfig.bucketName || '')
|
||||
return new API.S3plistApi(
|
||||
this.currentPicBedConfig.accessKeyId,
|
||||
this.currentPicBedConfig.secretAccessKey,
|
||||
this.currentPicBedConfig.endpoint,
|
||||
this.currentPicBedConfig.sslEnabled,
|
||||
this.currentPicBedConfig.s3ForcePathStyle,
|
||||
this.currentPicBedConfig.proxy,
|
||||
this.logger,
|
||||
this.currentPicBedConfig.dogeCloudSupport || false,
|
||||
this.currentPicBedConfig.bucketName || ''
|
||||
)
|
||||
case 'sftp':
|
||||
return new API.SftpApi(this.currentPicBedConfig.host, this.currentPicBedConfig.port, this.currentPicBedConfig.username, this.currentPicBedConfig.password, this.currentPicBedConfig.privateKey, this.currentPicBedConfig.passphrase, this.currentPicBedConfig.fileMode, this.currentPicBedConfig.dirMode, this.logger)
|
||||
return new API.SftpApi(
|
||||
this.currentPicBedConfig.host,
|
||||
this.currentPicBedConfig.port,
|
||||
this.currentPicBedConfig.username,
|
||||
this.currentPicBedConfig.password,
|
||||
this.currentPicBedConfig.privateKey,
|
||||
this.currentPicBedConfig.passphrase,
|
||||
this.currentPicBedConfig.fileMode,
|
||||
this.currentPicBedConfig.dirMode,
|
||||
this.logger
|
||||
)
|
||||
case 'tcyun':
|
||||
return new API.TcyunApi(this.currentPicBedConfig.secretId, this.currentPicBedConfig.secretKey, this.logger)
|
||||
case 'upyun':
|
||||
return new API.UpyunApi(this.currentPicBedConfig.bucketName, this.currentPicBedConfig.operator, this.currentPicBedConfig.password, this.logger, this.currentPicBedConfig.antiLeechToken, this.currentPicBedConfig.expireTime)
|
||||
return new API.UpyunApi(
|
||||
this.currentPicBedConfig.bucketName,
|
||||
this.currentPicBedConfig.operator,
|
||||
this.currentPicBedConfig.password,
|
||||
this.logger,
|
||||
this.currentPicBedConfig.antiLeechToken,
|
||||
this.currentPicBedConfig.expireTime
|
||||
)
|
||||
case 'webdavplist':
|
||||
return new API.WebdavplistApi(this.currentPicBedConfig.endpoint, this.currentPicBedConfig.username, this.currentPicBedConfig.password, this.currentPicBedConfig.sslEnabled, this.currentPicBedConfig.proxy, this.currentPicBedConfig.authType, this.logger)
|
||||
return new API.WebdavplistApi(
|
||||
this.currentPicBedConfig.endpoint,
|
||||
this.currentPicBedConfig.username,
|
||||
this.currentPicBedConfig.password,
|
||||
this.currentPicBedConfig.sslEnabled,
|
||||
this.currentPicBedConfig.proxy,
|
||||
this.currentPicBedConfig.authType,
|
||||
this.logger
|
||||
)
|
||||
default:
|
||||
return {} as any
|
||||
}
|
||||
}
|
||||
|
||||
private getPicBedConfig (picBedName: string): IPicBedMangeConfig {
|
||||
private getPicBedConfig(picBedName: string): IPicBedMangeConfig {
|
||||
return this.getConfig<IPicBedMangeConfig>(`picBed.${picBedName}`)
|
||||
}
|
||||
|
||||
private initConfigPath (): void {
|
||||
private initConfigPath(): void {
|
||||
if (this.configPath === '') {
|
||||
this.configPath = `${homedir()}/.piclist/manage.json`
|
||||
}
|
||||
@@ -103,30 +146,28 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
private initconfig (): void {
|
||||
private initconfig(): void {
|
||||
this.db = new ManageDB(this)
|
||||
this._config = this.db.read(true) as IManageConfigType
|
||||
}
|
||||
|
||||
getConfig<T> (name?: string): T {
|
||||
getConfig<T>(name?: string): T {
|
||||
if (!name) {
|
||||
return this._config as unknown as T
|
||||
}
|
||||
return get(this._config, name)
|
||||
}
|
||||
|
||||
saveConfig (config: IStringKeyMap): void {
|
||||
saveConfig(config: IStringKeyMap): void {
|
||||
if (!isInputConfigValid(config)) {
|
||||
this.logger.warn(
|
||||
'the format of config is invalid, please provide object'
|
||||
)
|
||||
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 {
|
||||
removeConfig(key: string, propName: string): void {
|
||||
if (!key || !propName) {
|
||||
return
|
||||
}
|
||||
@@ -134,11 +175,9 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
this.db.unset(key, propName)
|
||||
}
|
||||
|
||||
setConfig (config: IStringKeyMap): void {
|
||||
setConfig(config: IStringKeyMap): void {
|
||||
if (!isInputConfigValid(config)) {
|
||||
this.logger.warn(
|
||||
'the format of config is invalid, please provide object'
|
||||
)
|
||||
this.logger.warn('the format of config is invalid, please provide object')
|
||||
return
|
||||
}
|
||||
Object.keys(config).forEach((name: string) => {
|
||||
@@ -146,14 +185,12 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
})
|
||||
}
|
||||
|
||||
unsetConfig (key: string, propName: string): void {
|
||||
unsetConfig(key: string, propName: string): void {
|
||||
if (!key || !propName) return
|
||||
unset(this.getConfig(key), propName)
|
||||
}
|
||||
|
||||
async getBucketList (
|
||||
param?: IStringKeyMap | undefined
|
||||
): Promise<any> {
|
||||
async getBucketList(param?: IStringKeyMap | undefined): Promise<any> {
|
||||
let client
|
||||
const name = this.currentPicBedConfig.picBedName.replace('plist', '')
|
||||
switch (this.currentPicBedConfig.picBedName) {
|
||||
@@ -171,36 +208,36 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
return []
|
||||
}
|
||||
case 'upyun':
|
||||
return [{
|
||||
Name: this.currentPicBedConfig.bucketName,
|
||||
Location: 'upyun',
|
||||
CreationDate: new Date().toISOString()
|
||||
}]
|
||||
return [
|
||||
{
|
||||
Name: this.currentPicBedConfig.bucketName,
|
||||
Location: 'upyun',
|
||||
CreationDate: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
case 'smms':
|
||||
case 'webdavplist':
|
||||
case 'local':
|
||||
case 'sftp':
|
||||
return [{
|
||||
Name: name,
|
||||
Location: name,
|
||||
CreationDate: new Date().toISOString()
|
||||
}]
|
||||
return [
|
||||
{
|
||||
Name: name,
|
||||
Location: name,
|
||||
CreationDate: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
default:
|
||||
console.log(param)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async getBucketInfo (
|
||||
param?: IStringKeyMap | undefined
|
||||
): Promise<IStringKeyMap | IManageError> {
|
||||
async getBucketInfo(param?: IStringKeyMap | undefined): Promise<IStringKeyMap | IManageError> {
|
||||
console.log(param)
|
||||
return {}
|
||||
}
|
||||
|
||||
async getBucketDomain (
|
||||
param: IStringKeyMap
|
||||
): Promise<IStringKeyMap | IManageError> {
|
||||
async getBucketDomain(param: IStringKeyMap): Promise<IStringKeyMap | IManageError> {
|
||||
let client
|
||||
switch (this.currentPicBedConfig.picBedName) {
|
||||
case 'tcyun':
|
||||
@@ -225,9 +262,7 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
async createBucket (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async createBucket(param?: IStringKeyMap): Promise<boolean> {
|
||||
let client
|
||||
switch (this.currentPicBedConfig.picBedName) {
|
||||
case 'tcyun':
|
||||
@@ -246,44 +281,32 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteBucket (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async deleteBucket(param?: IStringKeyMap): Promise<boolean> {
|
||||
console.log(param)
|
||||
return false
|
||||
}
|
||||
|
||||
async getOperatorList (
|
||||
param?: IStringKeyMap
|
||||
): Promise<string[] | IManageError> {
|
||||
async getOperatorList(param?: IStringKeyMap): Promise<string[] | IManageError> {
|
||||
console.log(param)
|
||||
return []
|
||||
}
|
||||
|
||||
async addOperator (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async addOperator(param?: IStringKeyMap): Promise<boolean> {
|
||||
console.log(param)
|
||||
return false
|
||||
}
|
||||
|
||||
async deleteOperator (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async deleteOperator(param?: IStringKeyMap): Promise<boolean> {
|
||||
console.log(param)
|
||||
return false
|
||||
}
|
||||
|
||||
async getBucketAclPolicy (
|
||||
param?: IStringKeyMap
|
||||
): Promise<IStringKeyMap | IManageError> {
|
||||
async getBucketAclPolicy(param?: IStringKeyMap): Promise<IStringKeyMap | IManageError> {
|
||||
console.log(param)
|
||||
return {}
|
||||
}
|
||||
|
||||
async setBucketAclPolicy (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async setBucketAclPolicy(param?: IStringKeyMap): Promise<boolean> {
|
||||
let client
|
||||
switch (this.currentPicBedConfig.picBedName) {
|
||||
case 'qiniu':
|
||||
@@ -299,9 +322,7 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
async getBucketListRecursively (
|
||||
param?: IStringKeyMap
|
||||
): Promise<IStringKeyMap | IManageError> {
|
||||
async getBucketListRecursively(param?: IStringKeyMap): Promise<IStringKeyMap | IManageError> {
|
||||
let client
|
||||
let window
|
||||
const defaultResult = {
|
||||
@@ -344,9 +365,7 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
* @param param
|
||||
* @returns
|
||||
*/
|
||||
async getBucketListBackstage (
|
||||
param?: IStringKeyMap
|
||||
): Promise<IStringKeyMap | IManageError> {
|
||||
async getBucketListBackstage(param?: IStringKeyMap): Promise<IStringKeyMap | IManageError> {
|
||||
let client
|
||||
let window
|
||||
const defaultResult = {
|
||||
@@ -385,17 +404,15 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件夹列表
|
||||
* 结果统一进行格式化 文件夹提取到最前
|
||||
* key: 完整路径
|
||||
* fileName: 文件名
|
||||
* formatedTime: 格式化时间
|
||||
* isDir: 是否是文件夹
|
||||
* fileSize: 文件大小
|
||||
**/
|
||||
async getBucketFileList (
|
||||
param?: IStringKeyMap
|
||||
): Promise<IStringKeyMap | IManageError> {
|
||||
* 获取文件夹列表
|
||||
* 结果统一进行格式化 文件夹提取到最前
|
||||
* key: 完整路径
|
||||
* fileName: 文件名
|
||||
* formatedTime: 格式化时间
|
||||
* isDir: 是否是文件夹
|
||||
* fileSize: 文件大小
|
||||
**/
|
||||
async getBucketFileList(param?: IStringKeyMap): Promise<IStringKeyMap | IManageError> {
|
||||
const defaultResponse = {
|
||||
fullList: <any>[],
|
||||
isTruncated: false,
|
||||
@@ -422,9 +439,7 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteBucketFile (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async deleteBucketFile(param?: IStringKeyMap): Promise<boolean> {
|
||||
let client
|
||||
switch (this.currentPicBedConfig.picBedName) {
|
||||
case 'tcyun':
|
||||
@@ -451,9 +466,7 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteBucketFolder (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async deleteBucketFolder(param?: IStringKeyMap): Promise<boolean> {
|
||||
let client
|
||||
switch (this.currentPicBedConfig.picBedName) {
|
||||
case 'tcyun':
|
||||
@@ -477,9 +490,7 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
async renameBucketFile (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async renameBucketFile(param?: IStringKeyMap): Promise<boolean> {
|
||||
let client
|
||||
switch (this.currentPicBedConfig.picBedName) {
|
||||
case 'tcyun':
|
||||
@@ -502,9 +513,7 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
async downloadBucketFile (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async downloadBucketFile(param?: IStringKeyMap): Promise<boolean> {
|
||||
let client
|
||||
switch (this.currentPicBedConfig.picBedName) {
|
||||
case 'tcyun':
|
||||
@@ -531,16 +540,12 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
async copyMoveBucketFile (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async copyMoveBucketFile(param?: IStringKeyMap): Promise<boolean> {
|
||||
console.log(param)
|
||||
return false
|
||||
}
|
||||
|
||||
async createBucketFolder (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async createBucketFolder(param?: IStringKeyMap): Promise<boolean> {
|
||||
let client
|
||||
switch (this.currentPicBedConfig.picBedName) {
|
||||
case 'tcyun':
|
||||
@@ -564,9 +569,7 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
async uploadBucketFile (
|
||||
param?: IStringKeyMap
|
||||
): Promise<boolean> {
|
||||
async uploadBucketFile(param?: IStringKeyMap): Promise<boolean> {
|
||||
let client
|
||||
switch (this.currentPicBedConfig.picBedName) {
|
||||
case 'tcyun':
|
||||
@@ -592,9 +595,7 @@ export class ManageApi extends EventEmitter implements IManageApiType {
|
||||
}
|
||||
}
|
||||
|
||||
async getPreSignedUrl (
|
||||
param?: IStringKeyMap
|
||||
): Promise<string> {
|
||||
async getPreSignedUrl(param?: IStringKeyMap): Promise<string> {
|
||||
let client
|
||||
switch (this.currentPicBedConfig.picBedName) {
|
||||
case 'tcyun':
|
||||
|
||||
@@ -18,17 +18,12 @@ import { ManageLogger } from '~/manage/utils/logger'
|
||||
import { commonTaskStatus, downloadTaskSpecialStatus, uploadTaskSpecialStatus } from '#/types/enum'
|
||||
import { formatHttpProxy } from '#/utils/common'
|
||||
|
||||
export const getFSFile = async (
|
||||
filePath: string,
|
||||
stream: boolean = false
|
||||
): Promise<IStringKeyMap> => {
|
||||
export const getFSFile = async (filePath: string, stream: boolean = false): Promise<IStringKeyMap> => {
|
||||
try {
|
||||
return {
|
||||
extension: path.extname(filePath),
|
||||
fileName: path.basename(filePath),
|
||||
buffer: stream
|
||||
? fs.createReadStream(filePath)
|
||||
: await fs.readFile(filePath),
|
||||
buffer: stream ? fs.createReadStream(filePath) : await fs.readFile(filePath),
|
||||
success: true
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -38,10 +33,8 @@ export const getFSFile = async (
|
||||
}
|
||||
}
|
||||
|
||||
export function isInputConfigValid (config: any): boolean {
|
||||
return typeof config === 'object' &&
|
||||
!Array.isArray(config) &&
|
||||
Object.keys(config).length > 0
|
||||
export function isInputConfigValid(config: any): boolean {
|
||||
return typeof config === 'object' && !Array.isArray(config) && Object.keys(config).length > 0
|
||||
}
|
||||
|
||||
export const getFileMimeType = (filePath: string): string => mime.lookup(filePath) || 'application/octet-stream'
|
||||
@@ -83,17 +76,18 @@ export const clearTempFolder = () => fs.emptyDirSync(getTempDirPath())
|
||||
|
||||
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 hmacSha1Base64 = (secretKey: string, stringToSign: string): string =>
|
||||
crypto.createHmac('sha1', secretKey).update(Buffer.from(stringToSign, 'utf8')).digest('base64')
|
||||
|
||||
export const NewDownloader = async (
|
||||
instance: UpDownTaskQueue,
|
||||
preSignedUrl: string,
|
||||
id : string,
|
||||
id: string,
|
||||
savedFilePath: string,
|
||||
logger?: ManageLogger,
|
||||
proxy?: string,
|
||||
headers?: any
|
||||
) : Promise<boolean> => {
|
||||
): Promise<boolean> => {
|
||||
const options = {
|
||||
url: preSignedUrl,
|
||||
directory: path.dirname(savedFilePath),
|
||||
@@ -150,19 +144,16 @@ export const gotUpload = async (
|
||||
throwHttpErrors: boolean = false,
|
||||
agent: any = {}
|
||||
) => {
|
||||
got(
|
||||
url,
|
||||
{
|
||||
headers,
|
||||
method,
|
||||
body,
|
||||
timeout: {
|
||||
lookup: timeout
|
||||
},
|
||||
throwHttpErrors,
|
||||
agent
|
||||
}
|
||||
)
|
||||
got(url, {
|
||||
headers,
|
||||
method,
|
||||
body,
|
||||
timeout: {
|
||||
lookup: timeout
|
||||
},
|
||||
throwHttpErrors,
|
||||
agent
|
||||
})
|
||||
.on('uploadProgress', (progress: any) => {
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
@@ -174,7 +165,10 @@ export const gotUpload = async (
|
||||
instance.updateUploadTask({
|
||||
id,
|
||||
progress: res?.statusCode === 200 || res?.statusCode === 201 ? 100 : 0,
|
||||
status: res?.statusCode === 200 || res?.statusCode === 201 ? uploadTaskSpecialStatus.uploaded : commonTaskStatus.failed,
|
||||
status:
|
||||
res?.statusCode === 200 || res?.statusCode === 201
|
||||
? uploadTaskSpecialStatus.uploaded
|
||||
: commonTaskStatus.failed,
|
||||
finishTime: new Date().toLocaleString()
|
||||
})
|
||||
})
|
||||
@@ -190,7 +184,7 @@ export const gotUpload = async (
|
||||
})
|
||||
}
|
||||
|
||||
export const formatError = (err: any, params:IStringKeyMap) => {
|
||||
export const formatError = (err: any, params: IStringKeyMap) => {
|
||||
if (err instanceof RequestError) {
|
||||
return {
|
||||
...params,
|
||||
@@ -220,7 +214,10 @@ const commonOptions = {
|
||||
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined
|
||||
} as any
|
||||
|
||||
export const getAgent = (proxy:any, https: boolean = true): {
|
||||
export const getAgent = (
|
||||
proxy: any,
|
||||
https: boolean = true
|
||||
): {
|
||||
https?: HttpsProxyAgent
|
||||
http?: HttpProxyAgent
|
||||
} => {
|
||||
@@ -253,36 +250,36 @@ export const getInnerAgent = (proxy: any, sslEnabled: boolean = true) => {
|
||||
if (sslEnabled) {
|
||||
return formatProxy
|
||||
? {
|
||||
agent: new https.Agent({
|
||||
agent: new https.Agent({
|
||||
...commonOptions,
|
||||
rejectUnauthorized: false,
|
||||
host: formatProxy.host,
|
||||
port: formatProxy.port
|
||||
})
|
||||
}
|
||||
: {
|
||||
agent: new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
keepAlive: true
|
||||
})
|
||||
}
|
||||
}
|
||||
return formatProxy
|
||||
? {
|
||||
agent: new http.Agent({
|
||||
...commonOptions,
|
||||
rejectUnauthorized: false,
|
||||
host: formatProxy.host,
|
||||
port: formatProxy.port
|
||||
})
|
||||
}
|
||||
: {
|
||||
agent: new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
keepAlive: true
|
||||
: {
|
||||
agent: new http.Agent({
|
||||
...commonOptions
|
||||
})
|
||||
}
|
||||
}
|
||||
return formatProxy
|
||||
? {
|
||||
agent: new http.Agent({
|
||||
...commonOptions,
|
||||
host: formatProxy.host,
|
||||
port: formatProxy.port
|
||||
})
|
||||
}
|
||||
: {
|
||||
agent: new http.Agent({
|
||||
...commonOptions
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function getOptions (
|
||||
export function getOptions(
|
||||
method?: string,
|
||||
headers?: IStringKeyMap,
|
||||
searchParams?: IStringKeyMap,
|
||||
@@ -298,7 +295,9 @@ export function getOptions (
|
||||
...(body && { body }),
|
||||
...(responseType && { responseType }),
|
||||
...(timeout !== undefined ? { timeout: { request: timeout } } : { timeout: { request: 30000 } }),
|
||||
...(proxy && { agent: Object.fromEntries(Object.entries(getAgent(proxy)).filter(([, v]) => v !== undefined)) }),
|
||||
...(proxy && {
|
||||
agent: Object.fromEntries(Object.entries(getAgent(proxy)).filter(([, v]) => v !== undefined))
|
||||
}),
|
||||
throwHttpErrors: false
|
||||
}
|
||||
}
|
||||
@@ -309,14 +308,14 @@ export class ConcurrencyPromisePool {
|
||||
runningNum: number
|
||||
results: any[]
|
||||
|
||||
constructor (limit: number) {
|
||||
constructor(limit: number) {
|
||||
this.limit = limit
|
||||
this.queue = []
|
||||
this.runningNum = 0
|
||||
this.results = []
|
||||
}
|
||||
|
||||
all (promises: any[] = []) {
|
||||
all(promises: any[] = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
for (const promise of promises) {
|
||||
this._run(promise, resolve, reject)
|
||||
@@ -324,7 +323,7 @@ export class ConcurrencyPromisePool {
|
||||
})
|
||||
}
|
||||
|
||||
_run (promise: any, resolve: any, reject: any) {
|
||||
_run(promise: any, resolve: any, reject: any) {
|
||||
if (this.runningNum >= this.limit) {
|
||||
this.queue.push(promise)
|
||||
return
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const AliyunAreaCodeName : IStringKeyMap = {
|
||||
const AliyunAreaCodeName: IStringKeyMap = {
|
||||
'oss-cn-hangzhou': '华东1(杭州)',
|
||||
'oss-cn-shanghai': '华东2(上海)',
|
||||
'oss-cn-nanjing': '华东5(南京)',
|
||||
@@ -31,7 +31,7 @@ const AliyunAreaCodeName : IStringKeyMap = {
|
||||
'oss-rg-china-mainland': '无地域属性'
|
||||
}
|
||||
|
||||
const QiniuAreaCodeName : IStringKeyMap = {
|
||||
const QiniuAreaCodeName: IStringKeyMap = {
|
||||
z0: '华东-浙江',
|
||||
'cn-east-2': '华东 浙江2',
|
||||
z1: '华北-河北',
|
||||
@@ -42,7 +42,7 @@ const QiniuAreaCodeName : IStringKeyMap = {
|
||||
'ap-southeast-2': '亚太-河内'
|
||||
}
|
||||
|
||||
const TencentAreaCodeName : IStringKeyMap = {
|
||||
const TencentAreaCodeName: IStringKeyMap = {
|
||||
'ap-beijing-1': '北京一区',
|
||||
'ap-beijing': '北京',
|
||||
'ap-nanjing': '南京',
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface DogecloudToken {
|
||||
sessionToken: string
|
||||
}
|
||||
|
||||
export async function dogecloudApi (
|
||||
export async function dogecloudApi(
|
||||
apiPath: string,
|
||||
data = {},
|
||||
jsonMode: boolean = false,
|
||||
@@ -18,7 +18,10 @@ export async function dogecloudApi (
|
||||
secretKey: string
|
||||
) {
|
||||
const body = jsonMode ? JSON.stringify(data) : querystring.encode(data)
|
||||
const sign = crypto.createHmac('sha1', secretKey).update(Buffer.from(apiPath + '\n' + body, 'utf8')).digest('hex')
|
||||
const sign = crypto
|
||||
.createHmac('sha1', secretKey)
|
||||
.update(Buffer.from(apiPath + '\n' + body, 'utf8'))
|
||||
.digest('hex')
|
||||
const authorization = `TOKEN ${accessKey}:${sign}`
|
||||
try {
|
||||
const res = await axios.request({
|
||||
@@ -40,16 +43,22 @@ export async function dogecloudApi (
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTempToken (accessKey: string, secretKey: string): Promise<{} | DogecloudToken> {
|
||||
const dogeTempToken = await picgo.getConfig('Credentials.doge-token') || {} as any
|
||||
export async function getTempToken(accessKey: string, secretKey: string): Promise<IObj | DogecloudToken> {
|
||||
const dogeTempToken = (await picgo.getConfig('Credentials.doge-token')) || ({} as any)
|
||||
if (dogeTempToken.token && dogeTempToken.expires > Date.now() + 7200000) {
|
||||
return dogeTempToken.token
|
||||
}
|
||||
try {
|
||||
const data = await dogecloudApi('/auth/tmp_token.json', {
|
||||
channel: 'OSS_FULL',
|
||||
scopes: ['*']
|
||||
}, true, accessKey, secretKey)
|
||||
const data = await dogecloudApi(
|
||||
'/auth/tmp_token.json',
|
||||
{
|
||||
channel: 'OSS_FULL',
|
||||
scopes: ['*']
|
||||
},
|
||||
true,
|
||||
accessKey,
|
||||
secretKey
|
||||
)
|
||||
const token = data.Credentials
|
||||
picgo.saveConfig({
|
||||
Credentials: {
|
||||
|
||||
@@ -22,14 +22,12 @@ export class ManageLogger implements ILogger {
|
||||
#logLevel!: string
|
||||
#logPath!: string
|
||||
|
||||
constructor (ctx: IManageApiType) {
|
||||
constructor(ctx: IManageApiType) {
|
||||
this.#ctx = ctx
|
||||
}
|
||||
|
||||
#handleLog (type: ILogType, ...msg: ILogArgvTypeWithError[]): void {
|
||||
const logHeader = chalk[this.#level[type] as ILogColor](
|
||||
`[PicList ${type.toUpperCase()}]`
|
||||
)
|
||||
#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(configPaths.settings.logLevel)
|
||||
this.#logPath =
|
||||
@@ -53,7 +51,7 @@ export class ManageLogger implements ILogger {
|
||||
}, 0)
|
||||
}
|
||||
|
||||
#checkLogFileIsLarge (logPath: string): {
|
||||
#checkLogFileIsLarge(logPath: string): {
|
||||
isLarge: boolean
|
||||
logFileSize?: number
|
||||
logFileSizeLimit?: number
|
||||
@@ -61,11 +59,7 @@ export class ManageLogger implements ILogger {
|
||||
if (fs.existsSync(logPath)) {
|
||||
const logFileSize = fs.statSync(logPath).size
|
||||
const logFileSizeLimit =
|
||||
enforceNumber(
|
||||
this.#ctx.getConfig<Undefinable<number>>(
|
||||
configPaths.settings.logFileSizeLimit
|
||||
) || 10
|
||||
) *
|
||||
enforceNumber(this.#ctx.getConfig<Undefinable<number>>(configPaths.settings.logFileSizeLimit) || 10) *
|
||||
1024 *
|
||||
1024
|
||||
return {
|
||||
@@ -80,18 +74,14 @@ export class ManageLogger implements ILogger {
|
||||
}
|
||||
}
|
||||
|
||||
#recreateLogFile (logPath: string): void {
|
||||
#recreateLogFile(logPath: string): void {
|
||||
if (fs.existsSync(logPath)) {
|
||||
fs.unlinkSync(logPath)
|
||||
fs.createFileSync(logPath)
|
||||
}
|
||||
}
|
||||
|
||||
#handleWriteLog (
|
||||
logPath: string,
|
||||
type: string,
|
||||
...msg: ILogArgvTypeWithError[]
|
||||
): void {
|
||||
#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()}] `
|
||||
@@ -106,7 +96,7 @@ export class ManageLogger implements ILogger {
|
||||
}
|
||||
}
|
||||
|
||||
#formatLogItem (item: ILogArgvTypeWithError, type: string): string {
|
||||
#formatLogItem(item: ILogArgvTypeWithError, type: string): string {
|
||||
let result = ''
|
||||
if (item instanceof Error && type === 'error') {
|
||||
result += `\n------Error Stack Begin------\n${util.format(item?.stack)}\n-------Error Stack End------- `
|
||||
@@ -122,10 +112,7 @@ export class ManageLogger implements ILogger {
|
||||
return result
|
||||
}
|
||||
|
||||
#checkLogLevel (
|
||||
type: string,
|
||||
level: undefined | string | string[]
|
||||
): boolean {
|
||||
#checkLogLevel(type: string, level: undefined | string | string[]): boolean {
|
||||
if (level === undefined || level === 'all') {
|
||||
return true
|
||||
}
|
||||
@@ -135,23 +122,23 @@ export class ManageLogger implements ILogger {
|
||||
return type === level
|
||||
}
|
||||
|
||||
success (...msq: ILogArgvType[]): void {
|
||||
success(...msq: ILogArgvType[]): void {
|
||||
return this.#handleLog(ILogType.success, ...msq)
|
||||
}
|
||||
|
||||
info (...msq: ILogArgvType[]): void {
|
||||
info(...msq: ILogArgvType[]): void {
|
||||
return this.#handleLog(ILogType.info, ...msq)
|
||||
}
|
||||
|
||||
error (...msq: ILogArgvTypeWithError[]): void {
|
||||
error(...msq: ILogArgvTypeWithError[]): void {
|
||||
return this.#handleLog(ILogType.error, ...msq)
|
||||
}
|
||||
|
||||
warn (...msq: ILogArgvType[]): void {
|
||||
warn(...msq: ILogArgvType[]): void {
|
||||
return this.#handleLog(ILogType.warn, ...msq)
|
||||
}
|
||||
|
||||
debug (...msq: ILogArgvType[]): void {
|
||||
debug(...msq: ILogArgvType[]): void {
|
||||
if (isDev) {
|
||||
this.#handleLog(ILogType.info, ...msq)
|
||||
}
|
||||
|
||||
+12
-17
@@ -9,10 +9,7 @@ import picgo from '@core/picgo'
|
||||
import logger from '@core/picgo/logger'
|
||||
|
||||
import routers from '~/server/routerManager'
|
||||
import {
|
||||
handleResponse,
|
||||
ensureHTTPLink
|
||||
} from '~/server/utils'
|
||||
import { handleResponse, ensureHTTPLink } from '~/server/utils'
|
||||
|
||||
import { configPaths } from '#/utils/configPaths'
|
||||
|
||||
@@ -31,9 +28,7 @@ const multerStorage = multer.diskStorage({
|
||||
filename: function (_req: any, file: { originalname: any }, cb: (arg0: null, arg1: any) => void) {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
if (!/[^\u0000-\u00ff]/.test(file.originalname)) {
|
||||
file.originalname = Buffer.from(file.originalname, 'latin1').toString(
|
||||
'utf8'
|
||||
)
|
||||
file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf8')
|
||||
}
|
||||
cb(null, file.originalname)
|
||||
}
|
||||
@@ -47,12 +42,12 @@ class Server {
|
||||
#httpServer: http.Server
|
||||
#config: IServerConfig
|
||||
|
||||
constructor () {
|
||||
constructor() {
|
||||
this.#config = this.getConfigWithDefaults()
|
||||
this.#httpServer = http.createServer(this.#handleRequest)
|
||||
}
|
||||
|
||||
getConfigWithDefaults () {
|
||||
getConfigWithDefaults() {
|
||||
let config = picgo.getConfig<IServerConfig>(configPaths.settings.server)
|
||||
if (!this.#isValidConfig(config)) {
|
||||
config = { port: DEFAULT_PORT, host: DEFAULT_HOST, enable: true }
|
||||
@@ -62,8 +57,8 @@ class Server {
|
||||
return config
|
||||
}
|
||||
|
||||
#isValidConfig (config: IObj | undefined) {
|
||||
return config && config.port && config.host && (config.enable !== undefined)
|
||||
#isValidConfig(config: IObj | undefined) {
|
||||
return config && config.port && config.host && config.enable !== undefined
|
||||
}
|
||||
|
||||
#handleRequest = (request: http.IncomingMessage, response: http.ServerResponse) => {
|
||||
@@ -108,7 +103,7 @@ class Server {
|
||||
}
|
||||
}
|
||||
if (request.headers['content-type'] && request.headers['content-type'].startsWith('multipart/form-data')) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error since the multer type is not correct
|
||||
uploadMulter.any()(request, response, (err: any) => {
|
||||
if (err) {
|
||||
logger.info('[PicList Server]', err)
|
||||
@@ -120,7 +115,7 @@ class Server {
|
||||
}
|
||||
})
|
||||
}
|
||||
// @ts-ignore
|
||||
// @ts-expect-error since the multer type is not correct
|
||||
const list = request.files.map(file => file.path)
|
||||
logger.info('[PicList Server] get a formData request')
|
||||
const handler = routers.getHandler(url!, 'POST')?.handler
|
||||
@@ -140,7 +135,7 @@ class Server {
|
||||
})
|
||||
request.on('end', () => {
|
||||
try {
|
||||
postObj = (body === '') ? {} : JSON.parse(body)
|
||||
postObj = body === '' ? {} : JSON.parse(body)
|
||||
} catch (err: any) {
|
||||
logger.error('[PicList Server]', err)
|
||||
return handleResponse({
|
||||
@@ -205,20 +200,20 @@ class Server {
|
||||
})
|
||||
}
|
||||
|
||||
startup () {
|
||||
startup() {
|
||||
if (this.#config.enable) {
|
||||
this.#listen(this.#config.port)
|
||||
}
|
||||
}
|
||||
|
||||
shutdown (hasStarted?: boolean) {
|
||||
shutdown(hasStarted?: boolean) {
|
||||
this.#httpServer.close()
|
||||
if (!hasStarted) {
|
||||
logger.info('[PicList Server] shutdown')
|
||||
}
|
||||
}
|
||||
|
||||
restart () {
|
||||
restart() {
|
||||
this.shutdown()
|
||||
this.#config = this.getConfigWithDefaults()
|
||||
this.startup()
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
type HttpMethod = 'GET' | 'POST'
|
||||
|
||||
class Router {
|
||||
#router = new Map<string, Map<HttpMethod, {handler: routeHandler, urlparams?: URLSearchParams}>>()
|
||||
#router = new Map<string, Map<HttpMethod, { handler: routeHandler; urlparams?: URLSearchParams }>>()
|
||||
|
||||
#addRoute (method: HttpMethod, url: string, callback: routeHandler, urlparams?: URLSearchParams): void {
|
||||
#addRoute(method: HttpMethod, url: string, callback: routeHandler, urlparams?: URLSearchParams): void {
|
||||
if (!this.#router.has(url)) {
|
||||
this.#router.set(url, new Map())
|
||||
}
|
||||
this.#router.get(url)!.set(method, { handler: callback, urlparams })
|
||||
}
|
||||
|
||||
get (url: string, callback: routeHandler, urlparams?: URLSearchParams): void {
|
||||
get(url: string, callback: routeHandler, urlparams?: URLSearchParams): void {
|
||||
this.#addRoute('GET', url, callback, urlparams)
|
||||
}
|
||||
|
||||
post (url: string, callback: routeHandler, urlparams?: URLSearchParams): void {
|
||||
post(url: string, callback: routeHandler, urlparams?: URLSearchParams): void {
|
||||
this.#addRoute('POST', url, callback, urlparams)
|
||||
}
|
||||
|
||||
any (url: string, callback: routeHandler, urlparams?: URLSearchParams): void {
|
||||
any(url: string, callback: routeHandler, urlparams?: URLSearchParams): void {
|
||||
this.#addRoute('GET', url, callback, urlparams)
|
||||
this.#addRoute('POST', url, callback, urlparams)
|
||||
}
|
||||
|
||||
getHandler (url: string, method: HttpMethod) {
|
||||
getHandler(url: string, method: HttpMethod) {
|
||||
if (this.#router.has(url)) {
|
||||
const methods = this.#router.get(url)!
|
||||
if (methods.has(method)) {
|
||||
|
||||
+169
-176
@@ -16,10 +16,7 @@ import windowManager from 'apis/app/window/windowManager'
|
||||
|
||||
import { markdownContent } from '~/server/apiDoc'
|
||||
import router from '~/server/router'
|
||||
import {
|
||||
deleteChoosedFiles,
|
||||
handleResponse
|
||||
} from '~/server/utils'
|
||||
import { deleteChoosedFiles, handleResponse } from '~/server/utils'
|
||||
|
||||
import { configPaths } from '#/utils/configPaths'
|
||||
|
||||
@@ -32,7 +29,7 @@ const LOG_PATH = path.join(STORE_PATH, 'piclist.log')
|
||||
const errorMessage = `upload error. see ${LOG_PATH} for more detail.`
|
||||
const deleteErrorMessage = `delete error. see ${LOG_PATH} for more detail.`
|
||||
|
||||
async function responseForGet ({ response } : {response: http.ServerResponse}) {
|
||||
async function responseForGet({ response }: { response: http.ServerResponse }) {
|
||||
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
||||
const htmlContent = marked(markdownContent)
|
||||
response.write(htmlContent)
|
||||
@@ -42,192 +39,188 @@ async function responseForGet ({ response } : {response: http.ServerResponse}) {
|
||||
router.get('/', responseForGet)
|
||||
router.get('/upload', responseForGet)
|
||||
|
||||
router.post('/upload', async ({
|
||||
response,
|
||||
list = [],
|
||||
urlparams
|
||||
} : {
|
||||
response: IHttpResponse,
|
||||
list?: string[],
|
||||
urlparams?: URLSearchParams
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
const picbed = urlparams?.get('picbed')
|
||||
const passedKey = urlparams?.get('key')
|
||||
const serverKey = picgo.getConfig<string>(configPaths.settings.serverKey) || ''
|
||||
if (serverKey && passedKey !== serverKey) {
|
||||
router.post(
|
||||
'/upload',
|
||||
async ({
|
||||
response,
|
||||
list = [],
|
||||
urlparams
|
||||
}: {
|
||||
response: IHttpResponse
|
||||
list?: string[]
|
||||
urlparams?: URLSearchParams
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
const picbed = urlparams?.get('picbed')
|
||||
const passedKey = urlparams?.get('key')
|
||||
const serverKey = picgo.getConfig<string>(configPaths.settings.serverKey) || ''
|
||||
if (serverKey && passedKey !== serverKey) {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'server key is uncorrect'
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
let currentPicBedType = ''
|
||||
let currentPicBedConfig = {} as IStringKeyMap
|
||||
let currentPicBedConfigId = ''
|
||||
let needRestore = false
|
||||
if (picbed) {
|
||||
const currentPicBed = picgo.getConfig<IStringKeyMap>('picBed') || ({} as IStringKeyMap)
|
||||
currentPicBedType = currentPicBed.uploader || currentPicBed.current || 'smms'
|
||||
currentPicBedConfig = currentPicBed[currentPicBedType] || ({} as IStringKeyMap)
|
||||
currentPicBedConfigId = currentPicBedConfig._id
|
||||
const configName = urlparams?.get('configName') || currentPicBed[picbed]?._configName
|
||||
if (picbed === currentPicBedType && configName === currentPicBedConfig._configName) {
|
||||
// do nothing
|
||||
} else {
|
||||
needRestore = true
|
||||
const picBeds = picgo.getConfig<IStringKeyMap>('uploader')
|
||||
const currentPicBedList = picBeds?.[picbed]?.configList
|
||||
if (currentPicBedList) {
|
||||
const currentConfig = currentPicBedList?.find((item: any) => item._configName === configName)
|
||||
if (currentConfig) {
|
||||
changeCurrentUploader(picbed, currentConfig, currentConfig._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (list.length === 0) {
|
||||
// upload with clipboard
|
||||
logger.info('[PicList Server] upload clipboard file')
|
||||
const result = await uploadClipboardFiles()
|
||||
const res = result.url
|
||||
const fullResult = result.fullResult
|
||||
logger.info('[PicList Server] upload result:', res)
|
||||
if (res) {
|
||||
const treatedFullResult = {
|
||||
isEncrypted: 1,
|
||||
EncryptedData: new AESHelper().encrypt(JSON.stringify(fullResult)),
|
||||
...fullResult
|
||||
}
|
||||
delete treatedFullResult.config
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: true,
|
||||
result: [res],
|
||||
fullResult: [treatedFullResult]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.info('[PicList Server] upload files in list')
|
||||
// upload with files
|
||||
const pathList = list.map(item => {
|
||||
return {
|
||||
path: item
|
||||
}
|
||||
})
|
||||
const win = windowManager.getAvailableWindow()
|
||||
const result = await uploadChoosedFiles(win.webContents, pathList)
|
||||
const res = result.map(item => {
|
||||
return item.url
|
||||
})
|
||||
const fullResult = result.map((item: any) => {
|
||||
const treatedItem = {
|
||||
isEncrypted: 1,
|
||||
EncryptedData: new AESHelper().encrypt(JSON.stringify(item.fullResult)),
|
||||
...item.fullResult
|
||||
}
|
||||
delete treatedItem.config
|
||||
return treatedItem
|
||||
})
|
||||
logger.info('[PicList Server] upload result', res.join(' ; '))
|
||||
if (res.length) {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: true,
|
||||
result: res,
|
||||
fullResult
|
||||
}
|
||||
})
|
||||
} else {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
fs.emptyDirSync(serverTempDir)
|
||||
if (needRestore) {
|
||||
changeCurrentUploader(currentPicBedType, currentPicBedConfig, currentPicBedConfigId)
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'server key is uncorrect'
|
||||
message: errorMessage
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/delete',
|
||||
async ({ response, list = [] }: { response: IHttpResponse; list?: IStringKeyMap[] }): Promise<void> => {
|
||||
if (list.length === 0) {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'no file to delete'
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
let currentPicBedType = ''
|
||||
let currentPicBedConfig = {} as IStringKeyMap
|
||||
let currentPicBedConfigId = ''
|
||||
let needRestore = false
|
||||
if (picbed) {
|
||||
const currentPicBed = picgo.getConfig<IStringKeyMap>('picBed') || {} as IStringKeyMap
|
||||
currentPicBedType = currentPicBed.current || ''
|
||||
currentPicBedConfig = currentPicBed[currentPicBedType] || {} as IStringKeyMap
|
||||
currentPicBedConfigId = currentPicBedConfig._id
|
||||
const configName = urlparams?.get('configName') || currentPicBed[picbed]?._configName
|
||||
if (picbed === currentPicBedType && configName === currentPicBedConfig._configName) {
|
||||
// do nothing
|
||||
} else {
|
||||
needRestore = true
|
||||
const picBeds = picgo.getConfig<IStringKeyMap>('uploader')
|
||||
const currentPicBedList = picBeds?.[picbed]?.configList
|
||||
if (currentPicBedList) {
|
||||
const currentConfig = currentPicBedList?.find((item: any) => item._configName === configName)
|
||||
if (currentConfig) {
|
||||
changeCurrentUploader(picbed, currentConfig, currentConfig._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (list.length === 0) {
|
||||
// upload with clipboard
|
||||
logger.info('[PicList Server] upload clipboard file')
|
||||
const result = await uploadClipboardFiles()
|
||||
const res = result.url
|
||||
const fullResult = result.fullResult
|
||||
logger.info('[PicList Server] upload result:', res)
|
||||
if (res) {
|
||||
const treatedFullResult = {
|
||||
isEncrypted: 1,
|
||||
EncryptedData: new AESHelper().encrypt(JSON.stringify(fullResult)),
|
||||
...fullResult
|
||||
}
|
||||
delete treatedFullResult.config
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: true,
|
||||
result: [res],
|
||||
fullResult: [treatedFullResult]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.info('[PicList Server] upload files in list')
|
||||
// upload with files
|
||||
const pathList = list.map(item => {
|
||||
return {
|
||||
path: item
|
||||
try {
|
||||
const aesHelper = new AESHelper()
|
||||
const treatList = list.map(item => {
|
||||
if (!item.isEncrypted) return item
|
||||
return JSON.parse(aesHelper.decrypt(item.EncryptedData))
|
||||
})
|
||||
const result = await deleteChoosedFiles(treatList)
|
||||
const successCount = result.filter(item => item).length
|
||||
const failCount = result.length - successCount
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: !!successCount,
|
||||
message: successCount ? `delete success: ${successCount}, fail: ${failCount}` : deleteErrorMessage
|
||||
}
|
||||
})
|
||||
const win = windowManager.getAvailableWindow()
|
||||
const result = await uploadChoosedFiles(win.webContents, pathList)
|
||||
const res = result.map(item => {
|
||||
return item.url
|
||||
})
|
||||
const fullResult = result.map((item: any) => {
|
||||
const treatedItem = {
|
||||
isEncrypted: 1,
|
||||
EncryptedData: new AESHelper().encrypt(JSON.stringify(item.fullResult)),
|
||||
...item.fullResult
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: deleteErrorMessage
|
||||
}
|
||||
delete treatedItem.config
|
||||
return treatedItem
|
||||
})
|
||||
logger.info('[PicList Server] upload result', res.join(' ; '))
|
||||
if (res.length) {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: true,
|
||||
result: res,
|
||||
fullResult
|
||||
}
|
||||
})
|
||||
} else {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
fs.emptyDirSync(serverTempDir)
|
||||
if (needRestore) {
|
||||
changeCurrentUploader(currentPicBedType, currentPicBedConfig, currentPicBedConfigId)
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
router.post('/delete', async ({
|
||||
response,
|
||||
list = []
|
||||
} : {
|
||||
response: IHttpResponse,
|
||||
list?: IStringKeyMap[]
|
||||
}): Promise<void> => {
|
||||
if (list.length === 0) {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'no file to delete'
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
const aesHelper = new AESHelper()
|
||||
const treatList = list.map(item => {
|
||||
if (!item.isEncrypted) return item
|
||||
return JSON.parse(aesHelper.decrypt(item.EncryptedData))
|
||||
})
|
||||
const result = await deleteChoosedFiles(treatList)
|
||||
const successCount = result.filter(item => item).length
|
||||
const failCount = result.length - successCount
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: !!successCount,
|
||||
message: successCount ? `delete success: ${successCount}, fail: ${failCount}` : deleteErrorMessage
|
||||
}
|
||||
})
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: deleteErrorMessage
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
router.any('/heartbeat', async ({
|
||||
response
|
||||
} : {
|
||||
response: IHttpResponse,
|
||||
}) => {
|
||||
router.any('/heartbeat', async ({ response }: { response: IHttpResponse }) => {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
Notification
|
||||
} from 'electron'
|
||||
import { Notification } from 'electron'
|
||||
|
||||
import picgo from '@core/picgo'
|
||||
import logger from '@core/picgo/logger'
|
||||
@@ -28,10 +26,10 @@ export const handleResponse = ({
|
||||
body = {
|
||||
success: false
|
||||
}
|
||||
} : {
|
||||
response: IHttpResponse,
|
||||
statusCode?: number,
|
||||
header?: IObj,
|
||||
}: {
|
||||
response: IHttpResponse
|
||||
statusCode?: number
|
||||
header?: IObj
|
||||
body?: any
|
||||
}) => {
|
||||
if (body?.success === false) {
|
||||
@@ -43,9 +41,7 @@ export const handleResponse = ({
|
||||
}
|
||||
|
||||
export const ensureHTTPLink = (url: string): string => {
|
||||
return url.startsWith('http')
|
||||
? url
|
||||
: `http://${url}`
|
||||
return url.startsWith('http') ? url : `http://${url}`
|
||||
}
|
||||
|
||||
export const deleteChoosedFiles = async (list: ImgInfo[]): Promise<boolean[]> => {
|
||||
@@ -61,10 +57,7 @@ export const deleteChoosedFiles = async (list: ImgInfo[]): Promise<boolean[]> =>
|
||||
const noteFunc = (value: boolean) => {
|
||||
const notification = new Notification({
|
||||
title: T('MANAGE_BUCKET_BATCH_DELETE_ERROR_MSG_MSG2'),
|
||||
body: T(value
|
||||
? 'GALLERY_SYNC_DELETE_NOTICE_SUCCEED'
|
||||
: 'GALLERY_SYNC_DELETE_NOTICE_FAILED'
|
||||
)
|
||||
body: T(value ? 'GALLERY_SYNC_DELETE_NOTICE_SUCCEED' : 'GALLERY_SYNC_DELETE_NOTICE_FAILED')
|
||||
})
|
||||
notification.show()
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { configPaths } from '#/utils/configPaths'
|
||||
|
||||
const defaultPath = process.platform === 'win32' ? 'C:\\Users' : '/'
|
||||
|
||||
function generateDirectoryListingHtml (files: any[], requestPath: any) {
|
||||
function generateDirectoryListingHtml(files: any[], requestPath: any) {
|
||||
let html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><h1>Directory Listing</h1><ul>'
|
||||
files.forEach((file: string) => {
|
||||
const filePath = path.join(requestPath, file)
|
||||
@@ -20,7 +20,7 @@ function generateDirectoryListingHtml (files: any[], requestPath: any) {
|
||||
return html
|
||||
}
|
||||
|
||||
function serveDirectory (res: http.ServerResponse, filePath: fs.PathLike, requestPath: any) {
|
||||
function serveDirectory(res: http.ServerResponse, filePath: fs.PathLike, requestPath: any) {
|
||||
fs.readdir(filePath, (err, files) => {
|
||||
if (err) {
|
||||
res.writeHead(500)
|
||||
@@ -32,7 +32,7 @@ function serveDirectory (res: http.ServerResponse, filePath: fs.PathLike, reques
|
||||
})
|
||||
}
|
||||
|
||||
function serveFile (res:http.ServerResponse, filePath: fs.PathLike) {
|
||||
function serveFile(res: http.ServerResponse, filePath: fs.PathLike) {
|
||||
const readStream = fs.createReadStream(filePath)
|
||||
readStream.pipe(res)
|
||||
readStream.on('error', () => {
|
||||
@@ -45,12 +45,12 @@ class WebServer {
|
||||
#server!: http.Server
|
||||
#config!: IStringKeyMap
|
||||
|
||||
constructor () {
|
||||
constructor() {
|
||||
this.loadConfig()
|
||||
this.initServer()
|
||||
}
|
||||
|
||||
loadConfig (): void {
|
||||
loadConfig(): void {
|
||||
this.#config = {
|
||||
enableWebServer: picgo.getConfig<boolean>(configPaths.settings.enableWebServer) || false,
|
||||
webServerHost: picgo.getConfig<string>(configPaths.settings.webServerHost) || '0.0.0.0',
|
||||
@@ -59,7 +59,7 @@ class WebServer {
|
||||
}
|
||||
}
|
||||
|
||||
initServer (): void {
|
||||
initServer(): void {
|
||||
this.#server = http.createServer((req, res) => {
|
||||
const requestPath = req.url?.split('?')[0]
|
||||
const filePath = path.join(this.#config.webServerPath, decodeURIComponent(requestPath || ''))
|
||||
@@ -78,15 +78,19 @@ class WebServer {
|
||||
})
|
||||
}
|
||||
|
||||
start () {
|
||||
start() {
|
||||
if (this.#config.enableWebServer) {
|
||||
this.#server
|
||||
.listen(
|
||||
this.#config.webServerPort === 36699 ? 37777 : this.#config.webServerPort,
|
||||
this.#config.webServerHost, () => {
|
||||
logger.info(`Web server is running at http://${this.#config.webServerHost}:${this.#config.webServerPort}, root path is ${this.#config.webServerPath}`)
|
||||
})
|
||||
.on('error', (err) => {
|
||||
this.#config.webServerHost,
|
||||
() => {
|
||||
logger.info(
|
||||
`Web server is running at http://${this.#config.webServerHost}:${this.#config.webServerPort}, root path is ${this.#config.webServerPath}`
|
||||
)
|
||||
}
|
||||
)
|
||||
.on('error', err => {
|
||||
logger.error(err)
|
||||
})
|
||||
} else {
|
||||
@@ -94,13 +98,13 @@ class WebServer {
|
||||
}
|
||||
}
|
||||
|
||||
stop () {
|
||||
stop() {
|
||||
this.#server.close(() => {
|
||||
logger.info('Web server is stopped')
|
||||
})
|
||||
}
|
||||
|
||||
restart () {
|
||||
restart() {
|
||||
this.stop()
|
||||
this.loadConfig()
|
||||
this.initServer()
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DEFAULT_AES_PASSWORD } from '#/utils/static'
|
||||
export class AESHelper {
|
||||
key: Buffer
|
||||
|
||||
constructor () {
|
||||
constructor() {
|
||||
const userPassword = picgo.getConfig<string>(configPaths.settings.aesPassword) || DEFAULT_AES_PASSWORD
|
||||
const fixedSalt = Buffer.from('a8b3c4d2e4f5098712345678feedc0de', 'hex')
|
||||
const fixedIterations = 100000
|
||||
@@ -16,7 +16,7 @@ export class AESHelper {
|
||||
this.key = crypto.pbkdf2Sync(userPassword, fixedSalt, fixedIterations, keyLength, 'sha512')
|
||||
}
|
||||
|
||||
encrypt (plainText: string) {
|
||||
encrypt(plainText: string) {
|
||||
const iv = crypto.randomBytes(16)
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv)
|
||||
let encrypted = cipher.update(plainText, 'utf8', 'hex')
|
||||
@@ -24,7 +24,7 @@ export class AESHelper {
|
||||
return `${iv.toString('hex')}:${encrypted}`
|
||||
}
|
||||
|
||||
decrypt (encryptedData: string) {
|
||||
decrypt(encryptedData: string) {
|
||||
const [ivHex, encryptedText] = encryptedData.split(':')
|
||||
if (!ivHex || !encryptedText) {
|
||||
return '{}'
|
||||
|
||||
@@ -10,7 +10,7 @@ import { i18nManager } from '~/i18n'
|
||||
const configPath = dbPathChecker()
|
||||
const CONFIG_DIR = path.dirname(configPath)
|
||||
|
||||
function beforeOpen () {
|
||||
function beforeOpen() {
|
||||
if (process.platform === 'darwin') {
|
||||
resolveMacWorkFlow()
|
||||
}
|
||||
@@ -18,10 +18,7 @@ function beforeOpen () {
|
||||
resolveOtherI18nFiles()
|
||||
}
|
||||
|
||||
function copyFileOutsideOfElectronAsar (
|
||||
sourceInAsarArchive: string,
|
||||
destOutsideAsarArchive: string
|
||||
) {
|
||||
function copyFileOutsideOfElectronAsar(sourceInAsarArchive: string, destOutsideAsarArchive: string) {
|
||||
if (fs.existsSync(sourceInAsarArchive)) {
|
||||
// file will be copied
|
||||
if (fs.statSync(sourceInAsarArchive).isFile()) {
|
||||
@@ -45,7 +42,7 @@ function copyFileOutsideOfElectronAsar (
|
||||
/**
|
||||
* macOS 右键菜单
|
||||
*/
|
||||
function resolveMacWorkFlow () {
|
||||
function resolveMacWorkFlow() {
|
||||
const dest = `${os.homedir()}/Library/Services/Upload pictures with PicList.workflow`
|
||||
if (fs.existsSync(dest)) return true
|
||||
try {
|
||||
@@ -55,7 +52,7 @@ function resolveMacWorkFlow () {
|
||||
}
|
||||
}
|
||||
|
||||
function diffFilesAndUpdate (filePath1: string, filePath2: string) {
|
||||
function diffFilesAndUpdate(filePath1: string, filePath2: string) {
|
||||
try {
|
||||
const file1 = fs.existsSync(filePath1) && fs.readFileSync(filePath1)
|
||||
const file2 = fs.existsSync(filePath1) && fs.readFileSync(filePath2)
|
||||
@@ -72,7 +69,7 @@ function diffFilesAndUpdate (filePath1: string, filePath2: string) {
|
||||
/**
|
||||
* 初始化剪贴板生成图片的脚本
|
||||
*/
|
||||
function resolveClipboardImageGenerator () {
|
||||
function resolveClipboardImageGenerator() {
|
||||
const clipboardFiles = getClipboardFiles()
|
||||
if (!fs.pathExistsSync(path.join(CONFIG_DIR, 'windows10.ps1'))) {
|
||||
clipboardFiles.forEach(item => {
|
||||
@@ -84,14 +81,8 @@ function resolveClipboardImageGenerator () {
|
||||
})
|
||||
}
|
||||
|
||||
function getClipboardFiles () {
|
||||
const files = [
|
||||
'/linux.sh',
|
||||
'/mac.applescript',
|
||||
'/windows.ps1',
|
||||
'/windows10.ps1',
|
||||
'/wsl.sh'
|
||||
]
|
||||
function getClipboardFiles() {
|
||||
const files = ['/linux.sh', '/mac.applescript', '/windows.ps1', '/windows10.ps1', '/wsl.sh']
|
||||
|
||||
return files.map(item => {
|
||||
return {
|
||||
@@ -105,7 +96,7 @@ function resolveClipboardImageGenerator () {
|
||||
/**
|
||||
* 初始化其他语言文件
|
||||
*/
|
||||
function resolveOtherI18nFiles () {
|
||||
function resolveOtherI18nFiles() {
|
||||
const i18nFolder = path.join(CONFIG_DIR, 'i18n')
|
||||
if (!fs.pathExistsSync(i18nFolder)) {
|
||||
fs.mkdirSync(i18nFolder)
|
||||
|
||||
@@ -8,13 +8,13 @@ class ClipboardWatcher extends EventEmitter {
|
||||
timer: NodeJS.Timeout | null
|
||||
lastImageHash: string | null
|
||||
|
||||
constructor () {
|
||||
constructor() {
|
||||
super()
|
||||
this.lastImageHash = null
|
||||
this.timer = null
|
||||
}
|
||||
|
||||
startListening (watchDelay = 500) {
|
||||
startListening(watchDelay = 500) {
|
||||
this.stopListening(false)
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
@@ -33,7 +33,7 @@ class ClipboardWatcher extends EventEmitter {
|
||||
logger.info('Start to watch clipboard')
|
||||
}
|
||||
|
||||
stopListening (isLog = true) {
|
||||
stopListening(isLog = true) {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = null
|
||||
@@ -42,7 +42,7 @@ class ClipboardWatcher extends EventEmitter {
|
||||
isLog && logger.info('Stop to watch clipboard')
|
||||
}
|
||||
|
||||
getImageHash (image: Electron.NativeImage): string {
|
||||
getImageHash(image: Electron.NativeImage): string {
|
||||
const buffer = image.toBitmap()
|
||||
return crypto.createHash('md5').update(buffer).digest('hex')
|
||||
}
|
||||
|
||||
+27
-16
@@ -12,11 +12,13 @@ import { configPaths } from '#/utils/configPaths'
|
||||
|
||||
export let tray: Tray
|
||||
|
||||
export const setTray = (t: Tray) => { tray = t }
|
||||
export const setTray = (t: Tray) => {
|
||||
tray = t
|
||||
}
|
||||
|
||||
export const getTray = () => tray
|
||||
|
||||
export function setTrayToolTip (title: string): void {
|
||||
export function setTrayToolTip(title: string): void {
|
||||
if (tray) {
|
||||
tray.setToolTip(title)
|
||||
}
|
||||
@@ -32,13 +34,15 @@ export const handleCopyUrl = (str: string): void => {
|
||||
* show notification
|
||||
* @param options
|
||||
*/
|
||||
export const showNotification = (options: IPrivateShowNotificationOption = {
|
||||
title: '',
|
||||
body: '',
|
||||
clickToCopy: false,
|
||||
copyContent: '',
|
||||
clickFn: () => {}
|
||||
}) => {
|
||||
export const showNotification = (
|
||||
options: IPrivateShowNotificationOption = {
|
||||
title: '',
|
||||
body: '',
|
||||
clickToCopy: false,
|
||||
copyContent: '',
|
||||
clickFn: () => {}
|
||||
}
|
||||
) => {
|
||||
const notification = new Notification({
|
||||
title: options.title,
|
||||
body: options.body
|
||||
@@ -60,10 +64,8 @@ export const showNotification = (options: IPrivateShowNotificationOption = {
|
||||
}
|
||||
|
||||
export const showMessageBox = (options: any) => {
|
||||
return new Promise<IShowMessageBoxResult>(async (resolve) => {
|
||||
dialog.showMessageBox(
|
||||
options
|
||||
).then((res) => {
|
||||
return new Promise<IShowMessageBoxResult>(async resolve => {
|
||||
dialog.showMessageBox(options).then(res => {
|
||||
resolve({
|
||||
result: res.response,
|
||||
checkboxChecked: res.checkboxChecked
|
||||
@@ -118,14 +120,18 @@ export const getClipboardFilePath = (): string => {
|
||||
}
|
||||
|
||||
if (img.isEmpty() && platform === 'win32') {
|
||||
const imgPath = clipboard.readBuffer('FileNameW')?.toString('ucs2')?.replace(RegExp(String.fromCharCode(0), 'g'), '')
|
||||
const imgPath = clipboard
|
||||
.readBuffer('FileNameW')
|
||||
?.toString('ucs2')
|
||||
?.replace(RegExp(String.fromCharCode(0), 'g'), '')
|
||||
return imgPath || ''
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export const handleUrlEncodeWithSetting = (url: string) => db.get(configPaths.settings.encodeOutputURL) ? handleUrlEncode(url) : url
|
||||
export const handleUrlEncodeWithSetting = (url: string) =>
|
||||
db.get(configPaths.settings.encodeOutputURL) ? handleUrlEncode(url) : url
|
||||
|
||||
const c1nApi = 'https://c1n.cn/link/short'
|
||||
|
||||
@@ -163,7 +169,12 @@ const generateYOURLSShortUrl = async (url: string) => {
|
||||
if (!/^https?:\/\//.test(domain)) {
|
||||
domain = `http://${domain}`
|
||||
}
|
||||
const params = new URLSearchParams({ signature, action: 'shorturl', format: 'json', url })
|
||||
const params = new URLSearchParams({
|
||||
signature,
|
||||
action: 'shorturl',
|
||||
format: 'json',
|
||||
url
|
||||
})
|
||||
try {
|
||||
const res = await axios.get(`${domain}/yourls-api.php?${params.toString()}`)
|
||||
if (res.data?.shorturl) {
|
||||
|
||||
@@ -32,7 +32,7 @@ const dogeRegionMap: IStringKeyMap = {
|
||||
'ap-chengdu': '3'
|
||||
}
|
||||
|
||||
async function dogecloudApi (
|
||||
async function dogecloudApi(
|
||||
apiPath: string,
|
||||
data = {},
|
||||
jsonMode: boolean = false,
|
||||
@@ -40,7 +40,10 @@ async function dogecloudApi (
|
||||
secretKey: string
|
||||
) {
|
||||
const body = jsonMode ? JSON.stringify(data) : querystring.encode(data)
|
||||
const sign = crypto.createHmac('sha1', secretKey).update(Buffer.from(apiPath + '\n' + body, 'utf8')).digest('hex')
|
||||
const sign = crypto
|
||||
.createHmac('sha1', secretKey)
|
||||
.update(Buffer.from(apiPath + '\n' + body, 'utf8'))
|
||||
.digest('hex')
|
||||
const authorization = `TOKEN ${accessKey}:${sign}`
|
||||
try {
|
||||
const res = await axios.request({
|
||||
@@ -62,12 +65,18 @@ async function dogecloudApi (
|
||||
}
|
||||
}
|
||||
|
||||
async function getDogeToken (accessKey: string, secretKey: string): Promise<{} | DogecloudTokenFull> {
|
||||
async function getDogeToken(accessKey: string, secretKey: string): Promise<IObj | DogecloudTokenFull> {
|
||||
try {
|
||||
const data = await dogecloudApi('/auth/tmp_token.json', {
|
||||
channel: 'OSS_FULL',
|
||||
scopes: ['*']
|
||||
}, true, accessKey, secretKey)
|
||||
const data = await dogecloudApi(
|
||||
'/auth/tmp_token.json',
|
||||
{
|
||||
channel: 'OSS_FULL',
|
||||
scopes: ['*']
|
||||
},
|
||||
true,
|
||||
accessKey,
|
||||
secretKey
|
||||
)
|
||||
return data
|
||||
} catch (err: any) {
|
||||
console.log(err)
|
||||
@@ -75,10 +84,17 @@ async function getDogeToken (accessKey: string, secretKey: string): Promise<{} |
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode: boolean = false) {
|
||||
export async function removeFileFromS3InMain(configMap: IStringKeyMap, dogeMode: boolean = false) {
|
||||
try {
|
||||
const { url: rawUrl, type, config: { accessKeyID, secretAccessKey, bucketName, endpoint, pathStyleAccess, rejectUnauthorized, proxy } } = configMap
|
||||
let { imgUrl, config: { region } } = configMap
|
||||
const {
|
||||
url: rawUrl,
|
||||
type,
|
||||
config: { accessKeyID, secretAccessKey, bucketName, endpoint, pathStyleAccess, rejectUnauthorized, proxy }
|
||||
} = configMap
|
||||
let {
|
||||
imgUrl,
|
||||
config: { region }
|
||||
} = configMap
|
||||
if (type === 'aws-s3' || type === 'aws-s3-plist') {
|
||||
imgUrl = rawUrl || imgUrl || ''
|
||||
}
|
||||
@@ -105,21 +121,21 @@ export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode
|
||||
const extraOptions = sslEnabled ? { rejectUnauthorized: !!rejectUnauthorized } : {}
|
||||
const handler = sslEnabled
|
||||
? new NodeHttpHandler({
|
||||
httpsAgent: agent.https
|
||||
? agent.https
|
||||
: new https.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
httpsAgent: agent.https
|
||||
? agent.https
|
||||
: new https.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
: new NodeHttpHandler({
|
||||
httpAgent: agent.http
|
||||
? agent.http
|
||||
: new http.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
httpAgent: agent.http
|
||||
? agent.http
|
||||
: new http.Agent({
|
||||
...commonOptions,
|
||||
...extraOptions
|
||||
})
|
||||
})
|
||||
const s3Options: S3ClientConfig = {
|
||||
credentials: {
|
||||
accessKeyId: accessKeyID,
|
||||
@@ -139,6 +155,9 @@ export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode
|
||||
}
|
||||
}
|
||||
let result: any
|
||||
try {
|
||||
fileKey = decodeURIComponent(fileKey)
|
||||
} catch (err: any) {}
|
||||
try {
|
||||
const client = new S3Client(s3Options)
|
||||
const command = new DeleteObjectCommand({
|
||||
@@ -162,10 +181,12 @@ export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileFromDogeInMain (configMap: IStringKeyMap) {
|
||||
export async function removeFileFromDogeInMain(configMap: IStringKeyMap) {
|
||||
try {
|
||||
const { config: { bucketName, AccessKey, SecretKey } } = configMap
|
||||
const token = await getDogeToken(AccessKey, SecretKey) as DogecloudTokenFull
|
||||
const {
|
||||
config: { bucketName, AccessKey, SecretKey }
|
||||
} = configMap
|
||||
const token = (await getDogeToken(AccessKey, SecretKey)) as DogecloudTokenFull
|
||||
const bucket = token.Buckets?.find(item => item.name === bucketName || item.s3Bucket === bucketName)
|
||||
const newConfigMap = Object.assign({}, configMap)
|
||||
newConfigMap.config = {
|
||||
@@ -184,7 +205,7 @@ export async function removeFileFromDogeInMain (configMap: IStringKeyMap) {
|
||||
}
|
||||
}
|
||||
|
||||
function createHuaweiAuthorization (
|
||||
function createHuaweiAuthorization(
|
||||
bucketName: string,
|
||||
path: string,
|
||||
fileName: string,
|
||||
@@ -197,7 +218,7 @@ function createHuaweiAuthorization (
|
||||
return `OBS ${accessKey}:${singature}`
|
||||
}
|
||||
|
||||
export async function removeFileFromHuaweiInMain (configMap: IStringKeyMap) {
|
||||
export async function removeFileFromHuaweiInMain(configMap: IStringKeyMap) {
|
||||
const { fileName, config } = configMap
|
||||
const { accessKeyId, accessKeySecret, bucketName, endpoint } = config
|
||||
let path = config.path || '/'
|
||||
@@ -223,11 +244,11 @@ export async function removeFileFromHuaweiInMain (configMap: IStringKeyMap) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileFromSFTPInMain (config: ISftpPlistConfig, fileName: string) {
|
||||
export async function removeFileFromSFTPInMain(config: ISftpPlistConfig, fileName: string) {
|
||||
try {
|
||||
const client = SSHClient.instance
|
||||
await client.connect(config)
|
||||
const uploadPath = `/${(config.uploadPath || '')}/`.replace(/\/+/g, '/')
|
||||
const uploadPath = `/${config.uploadPath || ''}/`.replace(/\/+/g, '/')
|
||||
const remote = path.join(uploadPath, fileName)
|
||||
const deleteResult = await client.deleteFileSFTP(config, remote)
|
||||
client.close()
|
||||
|
||||
@@ -9,11 +9,8 @@ export const isMacOS = process.platform === 'darwin'
|
||||
|
||||
let version: string | undefined
|
||||
|
||||
const clean = (version: string) => version.split('.').length === 1
|
||||
? `${version}.0.0`
|
||||
: version.split('.').length === 2
|
||||
? `${version}.0`
|
||||
: version
|
||||
const clean = (version: string) =>
|
||||
version.split('.').length === 1 ? `${version}.0.0` : version.split('.').length === 2 ? `${version}.0` : version
|
||||
|
||||
const parseVersion = (plist: string) => {
|
||||
const matches = /<key>ProductVersion<\/key>\s*<string>([\d.]+)<\/string>/.exec(plist)
|
||||
@@ -24,7 +21,7 @@ const parseVersion = (plist: string) => {
|
||||
return matches[1].replace('10.16', '11')
|
||||
}
|
||||
|
||||
export function macOSVersion (): string {
|
||||
export function macOSVersion(): string {
|
||||
if (!isMacOS) return ''
|
||||
|
||||
if (!version) {
|
||||
@@ -45,7 +42,7 @@ if (process.env.NODE_ENV === 'test') {
|
||||
macOSVersion._parseVersion = parseVersion
|
||||
}
|
||||
|
||||
export function isMacOSVersion (semverRange: string) {
|
||||
export function isMacOSVersion(semverRange: string) {
|
||||
if (!isMacOS) {
|
||||
return false
|
||||
}
|
||||
@@ -55,7 +52,7 @@ export function isMacOSVersion (semverRange: string) {
|
||||
return semver.satisfies(macOSVersion(), clean(semverRange))
|
||||
}
|
||||
|
||||
export function isMacOSVersionGreaterThanOrEqualTo (version: string) {
|
||||
export function isMacOSVersionGreaterThanOrEqualTo(version: string) {
|
||||
if (!isMacOS) {
|
||||
return false
|
||||
}
|
||||
@@ -65,7 +62,7 @@ export function isMacOSVersionGreaterThanOrEqualTo (version: string) {
|
||||
return semver.gte(macOSVersion(), clean(version))
|
||||
}
|
||||
|
||||
export function assertMacOSVersion (semverRange: string) {
|
||||
export function assertMacOSVersion(semverRange: string) {
|
||||
semverRange = semverRange.replace('10.16', '11')
|
||||
|
||||
if (!isMacOSVersion(semverRange)) {
|
||||
@@ -73,7 +70,7 @@ export function assertMacOSVersion (semverRange: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function assertMacOSVersionGreaterThanOrEqualTo (version: string) {
|
||||
export function assertMacOSVersionGreaterThanOrEqualTo(version: string) {
|
||||
version = version.replace('10.16', '11')
|
||||
|
||||
if (!isMacOSVersionGreaterThanOrEqualTo(version)) {
|
||||
@@ -81,7 +78,7 @@ export function assertMacOSVersionGreaterThanOrEqualTo (version: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function assertMacOS () {
|
||||
export function assertMacOS() {
|
||||
if (!isMacOS) {
|
||||
throw new Error('Requires macOS')
|
||||
}
|
||||
|
||||
@@ -4,19 +4,21 @@ import { configPaths } from '#/utils/configPaths'
|
||||
const getPicBeds = () => {
|
||||
const picBedTypes = picgo.helper.uploader.getIdList()
|
||||
const picBedFromDB = picgo.getConfig<IPicBedType[]>(configPaths.picBed.list) || []
|
||||
const picBeds = picBedTypes.map((item: string) => {
|
||||
const visible = picBedFromDB.find((i: IPicBedType) => i.type === item) // object or undefined
|
||||
return {
|
||||
type: item,
|
||||
name: picgo.helper.uploader.get(item)!.name || item,
|
||||
visible: visible ? visible.visible : true
|
||||
}
|
||||
}).sort((a) => {
|
||||
if (a.type === 'tcyun') {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}) as IPicBedType[]
|
||||
const picBeds = picBedTypes
|
||||
.map((item: string) => {
|
||||
const visible = picBedFromDB.find((i: IPicBedType) => i.type === item) // object or undefined
|
||||
return {
|
||||
type: item,
|
||||
name: picgo.helper.uploader.get(item)!.name || item,
|
||||
visible: visible ? visible.visible : true
|
||||
}
|
||||
})
|
||||
.sort(a => {
|
||||
if (a.type === 'tcyun') {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}) as IPicBedType[]
|
||||
return picBeds
|
||||
}
|
||||
|
||||
|
||||
@@ -36,34 +36,34 @@ const getUploadFiles = (argv = process.argv, cwd = process.cwd(), logger: Logger
|
||||
if (fileList?.length === 0) {
|
||||
return null // for uploading images in clipboard
|
||||
} else if ((fileList?.length || 0) > 0) {
|
||||
const result = fileList!.map(item => {
|
||||
if (isUrl(item)) {
|
||||
return {
|
||||
path: item
|
||||
}
|
||||
}
|
||||
if (path.isAbsolute(item)) {
|
||||
return {
|
||||
path: item
|
||||
}
|
||||
} else {
|
||||
const tempPath = path.join(cwd, item)
|
||||
if (fs.existsSync(tempPath)) {
|
||||
const result = fileList!
|
||||
.map(item => {
|
||||
if (isUrl(item)) {
|
||||
return {
|
||||
path: tempPath
|
||||
path: item
|
||||
}
|
||||
}
|
||||
if (path.isAbsolute(item)) {
|
||||
return {
|
||||
path: item
|
||||
}
|
||||
} else {
|
||||
logger.warn(`cli -> can't get file: ${tempPath}, invalid path`)
|
||||
return null
|
||||
const tempPath = path.join(cwd, item)
|
||||
if (fs.existsSync(tempPath)) {
|
||||
return {
|
||||
path: tempPath
|
||||
}
|
||||
} else {
|
||||
logger.warn(`cli -> can't get file: ${tempPath}, invalid path`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}).filter(item => item !== null) as Result
|
||||
})
|
||||
.filter(item => item !== null) as Result
|
||||
return result
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export {
|
||||
getUploadFiles
|
||||
}
|
||||
export { getUploadFiles }
|
||||
|
||||
@@ -13,6 +13,7 @@ export const handleConfigWithFunction = (config: IPicGoPluginOriginConfig[]): IP
|
||||
config[i].default = config[i].default()
|
||||
}
|
||||
if (typeof config[i].choices === 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
config[i].choices = (config[i].choices as Function)()
|
||||
}
|
||||
}
|
||||
@@ -20,13 +21,17 @@ export const handleConfigWithFunction = (config: IPicGoPluginOriginConfig[]): IP
|
||||
}
|
||||
|
||||
export const completeUploaderMetaConfig = (originData: IStringKeyMap): IUploaderConfigListItem => {
|
||||
return Object.assign({
|
||||
_configName: 'Default'
|
||||
}, trimValues(originData), {
|
||||
_id: uuid(),
|
||||
_createdAt: Date.now(),
|
||||
_updatedAt: Date.now()
|
||||
})
|
||||
return Object.assign(
|
||||
{
|
||||
_configName: 'Default'
|
||||
},
|
||||
trimValues(originData),
|
||||
{
|
||||
_id: uuid(),
|
||||
_createdAt: Date.now(),
|
||||
_updatedAt: Date.now()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,7 +135,9 @@ export const deleteUploaderConfig = (type: string, id: string): IUploaderConfigI
|
||||
/**
|
||||
* upgrade old uploader config to new format
|
||||
*/
|
||||
export const upgradeUploaderConfig = (type: string): {
|
||||
export const upgradeUploaderConfig = (
|
||||
type: string
|
||||
): {
|
||||
configList: IStringKeyMap[]
|
||||
defaultId: string
|
||||
} => {
|
||||
|
||||
+60
-38
@@ -1,4 +1,3 @@
|
||||
// @ts-nocheck
|
||||
import fs from 'fs-extra'
|
||||
import { NodeSSH, Config, SSHExecCommandResponse } from 'node-ssh-no-cpu-features'
|
||||
import path from 'path'
|
||||
@@ -11,22 +10,26 @@ class SSHClient {
|
||||
private static _client: NodeSSH
|
||||
private _isConnected = false
|
||||
|
||||
static get instance (): SSHClient {
|
||||
static get instance(): SSHClient {
|
||||
return this._instance || (this._instance = new this())
|
||||
}
|
||||
|
||||
static get client (): NodeSSH {
|
||||
static get client(): NodeSSH {
|
||||
return this._client || (this._client = new NodeSSH())
|
||||
}
|
||||
|
||||
private changeWinStylePathToUnix (path: string): string {
|
||||
private changeWinStylePathToUnix(path: string): string {
|
||||
return path.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
async connect (config: ISftpPlistConfig): Promise<boolean> {
|
||||
async connect(config: ISftpPlistConfig): Promise<boolean> {
|
||||
const { username, password, privateKey, passphrase } = config
|
||||
const loginInfo: Config = privateKey
|
||||
? { username, privateKeyPath: privateKey, passphrase: passphrase || undefined }
|
||||
? {
|
||||
username,
|
||||
privateKeyPath: privateKey,
|
||||
passphrase: passphrase || undefined
|
||||
}
|
||||
: { username, password }
|
||||
try {
|
||||
await SSHClient.client.connect({
|
||||
@@ -41,52 +44,64 @@ class SSHClient {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFileSFTP (config: ISftpPlistConfig, remote: string): Promise<boolean> {
|
||||
async deleteFileSFTP(config: ISftpPlistConfig, remote: string): Promise<boolean> {
|
||||
try {
|
||||
const client = new Client()
|
||||
const { username, password, privateKey, passphrase } = config
|
||||
const loginInfo: Config = privateKey
|
||||
? { username, privateKey: fs.readFileSync(privateKey), passphrase: passphrase || undefined }
|
||||
? {
|
||||
username,
|
||||
privateKey: fs.readFileSync(privateKey),
|
||||
passphrase: passphrase || undefined
|
||||
}
|
||||
: { username, password }
|
||||
remote = this.changeWinStylePathToUnix(remote)
|
||||
if (remote === '/' || remote.includes('*')) return false
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
client.on('ready', () => {
|
||||
client.sftp((err, sftp) => {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
if (err) reject(false)
|
||||
sftp.unlink(remote, (err) => {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
if (err) reject(false)
|
||||
client.end()
|
||||
resolve(true)
|
||||
})
|
||||
client
|
||||
.on('ready', () => {
|
||||
client.sftp(
|
||||
(
|
||||
err: any,
|
||||
sftp: {
|
||||
unlink: (arg0: string, arg1: (err: any) => void) => void
|
||||
}
|
||||
) => {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
if (err) reject(false)
|
||||
sftp.unlink(remote, (err: any) => {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
if (err) reject(false)
|
||||
client.end()
|
||||
resolve(true)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}).connect({
|
||||
host: config.host,
|
||||
port: Number(config.port) || 22,
|
||||
...loginInfo
|
||||
})
|
||||
}
|
||||
)
|
||||
return await promise
|
||||
.connect({
|
||||
host: config.host,
|
||||
port: Number(config.port) || 22,
|
||||
...loginInfo
|
||||
})
|
||||
})
|
||||
return (await promise) as boolean
|
||||
} catch (err: any) {
|
||||
console.log(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private async exec (script: string): Promise<boolean> {
|
||||
private async exec(script: string): Promise<boolean> {
|
||||
const execResult = await SSHClient.client.execCommand(script)
|
||||
return execResult.code === 0
|
||||
}
|
||||
|
||||
async execCommand (script: string): Promise<SSHExecCommandResponse> {
|
||||
async execCommand(script: string): Promise<SSHExecCommandResponse> {
|
||||
const execResult = await SSHClient.client.execCommand(script)
|
||||
return execResult || { code: 1, stdout: '', stderr: '' }
|
||||
}
|
||||
|
||||
async getFile (local: string, remote: string): Promise<boolean> {
|
||||
async getFile(local: string, remote: string): Promise<boolean> {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('SSH 未连接')
|
||||
}
|
||||
@@ -103,10 +118,14 @@ class SSHClient {
|
||||
}
|
||||
}
|
||||
|
||||
async putFile (local: string, remote: string, config: {
|
||||
fileMode?: string
|
||||
dirMode?: string
|
||||
} = {}): Promise<boolean> {
|
||||
async putFile(
|
||||
local: string,
|
||||
remote: string,
|
||||
config: {
|
||||
fileMode?: string
|
||||
dirMode?: string
|
||||
} = {}
|
||||
): Promise<boolean> {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('SSH 未连接')
|
||||
}
|
||||
@@ -126,9 +145,12 @@ class SSHClient {
|
||||
}
|
||||
}
|
||||
|
||||
async mkdir (dirPath: string, config: {
|
||||
dirMode?: string
|
||||
} = {}): Promise<boolean> {
|
||||
async mkdir(
|
||||
dirPath: string,
|
||||
config: {
|
||||
dirMode?: string
|
||||
} = {}
|
||||
): Promise<boolean> {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('SSH 未连接')
|
||||
}
|
||||
@@ -158,11 +180,11 @@ class SSHClient {
|
||||
}
|
||||
}
|
||||
|
||||
get isConnected (): boolean {
|
||||
get isConnected(): boolean {
|
||||
return SSHClient.client.isConnected()
|
||||
}
|
||||
|
||||
close (): void {
|
||||
close(): void {
|
||||
SSHClient.client.dispose()
|
||||
this._isConnected = false
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user