Merge branch 'dev' into release

This commit is contained in:
Kuingsmile
2024-07-07 14:27:26 +08:00
179 changed files with 27528 additions and 28978 deletions
+9 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
+34 -35
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+33 -33
View File
@@ -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
View File
@@ -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.
+167 -174
View File
@@ -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
```
+254 -254
View File
@@ -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>
![Alt](https://repobeats.axiom.co/api/embed/9e4ec90b7b50f8e9c10d77439e49e26b303fabed.svg "Repobeats analytics image")
[简体中文](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:
![image](https://user-images.githubusercontent.com/96409857/226522101-b3531b7b-534c-4149-b527-8738d4ebb041.png)
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:
![image](https://user-images.githubusercontent.com/96409857/226645570-4c6e06a5-5bd9-40c3-a21d-8446c66325e3.png)
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.
![image](https://user-images.githubusercontent.com/96409857/226522718-8378c480-9fb4-4785-87e1-d59808862016.png)
### 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
![image](https://github.com/Kuingsmile/PicList/assets/96409857/1b76c0c4-753c-4d66-aa24-f805f9c2da15)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/56cf838a-a2eb-40af-96d4-1ffea25400af)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/bca7688a-e07f-4e80-9edd-c224298fa8ab)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/3e48e03d-b0b2-49e2-92a6-a52e0884677d)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/29de0046-1aef-4b28-95a6-b26c6e297c6f)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/e1c04488-2d3a-4e8f-aa26-ce41d0a383e2)
## 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)
![tg](https://pichoro.msq.pub/wechat.png)
## 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 [![GitHub stars](https://img.shields.io/github/stars/kuingsmile/PicList?logo=github&style=social)](https://github.com/kuingsmile/PicList/stargazers)
[![Stargazers over time](https://starchart.cc/kuingsmile/PicList.svg)](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>
![Alt](https://repobeats.axiom.co/api/embed/9e4ec90b7b50f8e9c10d77439e49e26b303fabed.svg "Repobeats analytics image")
[简体中文](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:
![image](https://user-images.githubusercontent.com/96409857/226522101-b3531b7b-534c-4149-b527-8738d4ebb041.png)
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:
![image](https://user-images.githubusercontent.com/96409857/226645570-4c6e06a5-5bd9-40c3-a21d-8446c66325e3.png)
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.
![image](https://user-images.githubusercontent.com/96409857/226522718-8378c480-9fb4-4785-87e1-d59808862016.png)
### 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
![image](https://github.com/Kuingsmile/PicList/assets/96409857/1b76c0c4-753c-4d66-aa24-f805f9c2da15)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/56cf838a-a2eb-40af-96d4-1ffea25400af)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/bca7688a-e07f-4e80-9edd-c224298fa8ab)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/3e48e03d-b0b2-49e2-92a6-a52e0884677d)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/29de0046-1aef-4b28-95a6-b26c6e297c6f)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/e1c04488-2d3a-4e8f-aa26-ce41d0a383e2)
## 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)
![tg](https://pichoro.msq.pub/wechat.png)
## 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 [![GitHub stars](https://img.shields.io/github/stars/kuingsmile/PicList?logo=github&style=social)](https://github.com/kuingsmile/PicList/stargazers)
[![Stargazers over time](https://starchart.cc/kuingsmile/PicList.svg)](https://github.com/kuingsmile/PicList/stargazers)
+260 -244
View File
@@ -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>
![Alt](https://repobeats.axiom.co/api/embed/9e4ec90b7b50f8e9c10d77439e49e26b303fabed.svg "Repobeats analytics image")
简体中文 | [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和PicGoapp)的路径为PicList的安装路径,已经在最新的Dev版本中修复。
[下载地址](https://typora.io/releases/all)
#### 1.6.0版本以下
Windows:
进入Typora设置界面,选择图像,将上传服务设置为 `PicGo(app)`,然后在 `PicGo路径`中填写PicList的安装路径,如下图所示:
![image](https://user-images.githubusercontent.com/96409857/226522101-b3531b7b-534c-4149-b527-8738d4ebb041.png)
或者,您也可以使用 `npm install piclist`命令安装PicList-core,然后上传服务设置为 `PicGo-Core(command line)`
MacOS:
进入Typora设置界面,选择图像,将上传服务设置为 `Custom Command`,然后在 `Command`中填写 `/Applications/PicList.app/Contents/MacOS/PicList upload`,如下图所示:
![image](https://user-images.githubusercontent.com/96409857/226645570-4c6e06a5-5bd9-40c3-a21d-8446c66325e3.png)
验证上传选项可能会出现问题,可以忽略,直接使用是正常的。
### 如何在Obsidian中使用
在社区插件中搜索安装 `Image auto upload Plugin`,然后进入插件设置页面,修改默认上传器为 `PicGo(app)`,设置 `PicGo server``http://127.0.0.1:36677/upload`即可,如下图所示, 此外该插件还额外支持通过PicList进行云端删除,请在删除接口内填入 `http://127.0.0.1:36677/delete`
![image](https://user-images.githubusercontent.com/96409857/226522718-8378c480-9fb4-4785-87e1-d59808862016.png)
### 如何在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
```
## 应用截图
![image](https://github.com/Kuingsmile/PicList/assets/96409857/1b76c0c4-753c-4d66-aa24-f805f9c2da15)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/56cf838a-a2eb-40af-96d4-1ffea25400af)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/bca7688a-e07f-4e80-9edd-c224298fa8ab)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/3e48e03d-b0b2-49e2-92a6-a52e0884677d)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/29de0046-1aef-4b28-95a6-b26c6e297c6f)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/e1c04488-2d3a-4e8f-aa26-ce41d0a383e2)
## 开发说明
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)
![tg](https://pichoro.msq.pub/wechat.png)
## License
本项目基于MIT协议开源,欢迎大家使用和贡献代码,感谢原作者Molunerfinn的开源精神。
[MIT](https://opensource.org/licenses/MIT)
Copyright (c) 2017-present Molunerfinn
Copyright (c) 2023-present Kuingsmile
## Star Me
- Star 趋势 [![GitHub stars](https://img.shields.io/github/stars/kuingsmile/PicList?logo=github&style=social)](https://github.com/kuingsmile/PicList/stargazers)
[![Stargazers over time](https://starchart.cc/kuingsmile/PicList.svg)](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>
![Alt](https://repobeats.axiom.co/api/embed/9e4ec90b7b50f8e9c10d77439e49e26b303fabed.svg "Repobeats analytics image")
简体中文 | [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和PicGoapp)的路径为PicList的安装路径,已经在最新的Dev版本中修复。
[下载地址](https://typora.io/releases/all)
#### 1.6.0版本以下
Windows:
进入Typora设置界面,选择图像,将上传服务设置为 `PicGo(app)`,然后在 `PicGo路径`中填写PicList的安装路径,如下图所示:
![image](https://user-images.githubusercontent.com/96409857/226522101-b3531b7b-534c-4149-b527-8738d4ebb041.png)
或者,您也可以使用 `npm install piclist`命令安装PicList-core,然后上传服务设置为 `PicGo-Core(command line)`
MacOS:
进入Typora设置界面,选择图像,将上传服务设置为 `Custom Command`,然后在 `Command`中填写 `/Applications/PicList.app/Contents/MacOS/PicList upload`,如下图所示:
![image](https://user-images.githubusercontent.com/96409857/226645570-4c6e06a5-5bd9-40c3-a21d-8446c66325e3.png)
验证上传选项可能会出现问题,可以忽略,直接使用是正常的。
### 如何在Obsidian中使用
在社区插件中搜索安装 `Image auto upload Plugin`,然后进入插件设置页面,修改默认上传器为 `PicGo(app)`,设置 `PicGo server``http://127.0.0.1:36677/upload`即可,如下图所示, 此外该插件还额外支持通过PicList进行云端删除,请在删除接口内填入 `http://127.0.0.1:36677/delete`
![image](https://user-images.githubusercontent.com/96409857/226522718-8378c480-9fb4-4785-87e1-d59808862016.png)
### 如何在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系统识别为“恶意软件”,不会出现“文件已损坏”提示。
## 应用截图
![image](https://github.com/Kuingsmile/PicList/assets/96409857/1b76c0c4-753c-4d66-aa24-f805f9c2da15)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/56cf838a-a2eb-40af-96d4-1ffea25400af)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/bca7688a-e07f-4e80-9edd-c224298fa8ab)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/3e48e03d-b0b2-49e2-92a6-a52e0884677d)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/29de0046-1aef-4b28-95a6-b26c6e297c6f)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/e1c04488-2d3a-4e8f-aa26-ce41d0a383e2)
## 开发说明
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)
![tg](https://pichoro.msq.pub/wechat.png)
## License
本项目基于MIT协议开源,欢迎大家使用和贡献代码,感谢原作者Molunerfinn的开源精神。
[MIT](https://opensource.org/licenses/MIT)
Copyright (c) 2017-present Molunerfinn
Copyright (c) 2023-present Kuingsmile
## Star Me
- Star 趋势 [![GitHub stars](https://img.shields.io/github/stars/kuingsmile/PicList?logo=github&style=social)](https://github.com/kuingsmile/PicList/stargazers)
[![Stargazers over time](https://starchart.cc/kuingsmile/PicList.svg)](https://github.com/kuingsmile/PicList/stargazers)
+3 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1004 -1004
View File
File diff suppressed because it is too large Load Diff
+997 -997
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -51,4 +51,5 @@ app.use(pinia)
app.use(hljsVuePlugin)
app.use(VueVideoPlayer)
app.mount('#app')
initTalkingData()
+35 -41
View File
@@ -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 }
+16 -14
View File
@@ -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()]
}
}
+80 -30
View File
@@ -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)
}
+26 -15
View File
@@ -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'),
+35 -43
View File
@@ -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)
+15 -14
View File
@@ -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'
+20 -12
View File
@@ -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)
}
}
+4 -4
View File
@@ -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
+9 -7
View File
@@ -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)
})
+11 -12
View File
@@ -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 }
+15 -17
View File
@@ -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 }
+1 -1
View File
@@ -21,7 +21,7 @@ picgo.GUI_VERSION = global.PICGO_GUI_VERSION
const originPicGoSaveConfig = picgo.saveConfig.bind(picgo)
function flushDB () {
function flushDB() {
db.read(true)
}
+1 -3
View File
@@ -94,6 +94,4 @@ const getLogger = (logPath: string, logType: string) => {
}
}
export {
getLogger
}
export { getLogger }
+71 -66
View File
@@ -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)
}
})
})
}
})
+8 -13
View File
@@ -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
View File
@@ -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 }
+7 -7
View File
@@ -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 }
+1 -1
View File
@@ -20,7 +20,7 @@ export class RPCRouter implements IRPCRouter {
return this
}
routes () {
routes() {
return this.routeMap
}
}
+8 -6
View File
@@ -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 }
+100
View File
@@ -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
}
]
+1 -3
View File
@@ -94,6 +94,4 @@ const picbedRoutes = [
picbedRouter.addBatch(picbedRoutes)
export {
picbedRouter
}
export { picbedRouter }
+1 -4
View File
@@ -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 }
+12 -10
View File
@@ -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)
+2 -9
View File
@@ -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'
+2 -7
View File
@@ -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 }
+1 -6
View File
@@ -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,
+4 -5
View File
@@ -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 -1
View File
@@ -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,
+7 -8
View File
@@ -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 }
+1 -3
View File
@@ -30,6 +30,4 @@ const uploadRoutes = [
uploadRouter.addBatch(uploadRoutes)
export {
uploadRouter
}
export { uploadRouter }
+9 -7
View File
@@ -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')
})
+8 -8
View File
@@ -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)
+2 -2
View File
@@ -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
}
+4 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+33 -35
View File
@@ -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
+18 -16
View File
@@ -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
View File
@@ -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
View File
@@ -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
+15 -15
View File
@@ -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) {
+52 -58
View File
@@ -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
View File
@@ -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
}
+74 -61
View File
@@ -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
+67 -53
View File
@@ -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
+8 -8
View File
@@ -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])
})
+16 -12
View File
@@ -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 }
+57 -38
View File
@@ -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
})
)
}
}
}
-3
View File
@@ -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'
-155
View File
@@ -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
})
}
}
-36
View File
@@ -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
View File
@@ -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':
+54 -55
View File
@@ -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
+3 -3
View File
@@ -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': '南京',
+17 -8
View File
@@ -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: {
+14 -27
View File
@@ -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
View File
@@ -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()
+6 -6
View File
@@ -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
View File
@@ -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: {
+7 -14
View File
@@ -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()
}
+17 -13
View File
@@ -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()
+3 -3
View File
@@ -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 '{}'
+8 -17
View File
@@ -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)
+4 -4
View File
@@ -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
View File
@@ -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) {
+52 -31
View File
@@ -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()
+8 -11
View File
@@ -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')
}
+15 -13
View File
@@ -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
}
+21 -21
View File
@@ -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 }
+15 -8
View File
@@ -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
View File
@@ -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