Merge commit '9d63799d5a60b61eedfc1e2d35efe6cc93688a1d' into release

This commit is contained in:
萌萌哒赫萝
2023-08-23 00:14:23 -07:00
127 changed files with 5959 additions and 3329 deletions
+86
View File
@@ -0,0 +1,86 @@
# main.yml
# Workflow's name
name: Mac Beta Build
# Workflow's trigger
on:
workflow_dispatch:
env:
ELECTRON_OUTPUT_PATH: ./dist_electron
CSC_LINK: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.P12_PASSWORD }}
jobs:
release:
name: build and release electron app
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-11]
steps:
- name: Check out git repository
uses: actions/checkout@v2
# step2: sign
- name: Install the Apple certificates
if: matrix.os == 'macos-11'
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
# step3: install node env
- name: Install Node.js
uses: actions/setup-node@v2
with:
node-version: '16.x'
- name: Install system deps
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils
# step3: yarn
- name: Yarn install macos
if: matrix.os == 'macos-11'
run: |
yarn
yarn global add xvfb-maybe
npm rebuild --platform=darwin --arch=arm64 sharp
- name: Yarn install windows
if: matrix.os == 'windows-latest'
run: |
yarn
yarn global add xvfb-maybe
- name: Yarn install linux
if: matrix.os == 'ubuntu-latest'
run: |
yarn
yarn global add xvfb-maybe
- name: Build & release app
run: |
yarn run build
yarn upload-beta
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
R2_SECRET_ID: ${{ secrets.R2_SECRET_ID }}
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
ELECTRON_SKIP_NOTARIZATION: ${{ secrets.ELECTRON_SKIP_NOTARIZATION }}
XCODE_APP_LOADER_EMAIL: ${{ secrets.XCODE_APP_LOADER_EMAIL }}
XCODE_APP_LOADER_PASSWORD: ${{ secrets.XCODE_APP_LOADER_PASSWORD }}
XCODE_TEAM_ID: ${{ secrets.XCODE_TEAM_ID }}
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
-52
View File
@@ -1,52 +0,0 @@
# Commented sections below can be used to run tests on the CI server
# https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing
osx_image: xcode8.3
sudo: required
dist: trusty
language: c
matrix:
include:
- os: osx
- os: linux
env: CC=clang CXX=clang++ npm_config_clang=1
compiler: clang
cache:
directories:
- node_modules
- "$HOME/.electron"
- "$HOME/.cache"
addons:
apt:
packages:
- libgnome-keyring-dev
- icnsutils
#- xvfb
before_install:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install git-lfs; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi
install:
#- export DISPLAY=':99.0'
#- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
- nvm install 10
- curl -o- -L https://yarnpkg.com/install.sh | bash
- source ~/.bashrc
- npm install -g xvfb-maybe
- yarn
script:
#- xvfb-maybe node_modules/.bin/karma start test/unit/karma.conf.js
#- yarn run pack && xvfb-maybe node_modules/.bin/mocha test/e2e
- npm run release
# - yarn run build:docs
before_script:
- git lfs pull
branches:
only:
- master
# after_script:
# - cd docs/dist
# - git init
# - git config user.name "Molunerfinn"
# - git config user.email "marksz@teamsz.xyz"
# - git add .
# - git commit -m "Travis build docs"
# - git push --force --quiet "https://${GH_TOKEN}@github.com/Molunerfinn/PicGo.git" master:gh-pages
-1
View File
@@ -24,7 +24,6 @@ yarn dev
5. 与图床管理功能相关的代码请在`src/main/manage``src/renderer/manage`目录下添加。 5. 与图床管理功能相关的代码请在`src/main/manage``src/renderer/manage`目录下添加。
## i18n ## i18n
1.`public/i18n/` 下面创建一种语言的 `yml` 文件,例如 `zh-Hans.yml`。然后参考 `zh-CN.yml` 或者 `en.yml` 编写语言文件。并注意,PicList 会通过语言文件中的 `LANG_DISPLAY_LABEL` 向用户展示该语言的名称。 1.`public/i18n/` 下面创建一种语言的 `yml` 文件,例如 `zh-Hans.yml`。然后参考 `zh-CN.yml` 或者 `en.yml` 编写语言文件。并注意,PicList 会通过语言文件中的 `LANG_DISPLAY_LABEL` 向用户展示该语言的名称。
+20 -1
View File
@@ -27,8 +27,13 @@ PicList所有新功能的添加没有影响到PicGo的原有功能,所以你
- SM.MS - SM.MS
- Imgur - Imgur
- GitHub - GitHub
- Webdav - WebDav
- Aws S3 - Aws S3
- 本地路径
- 内置SFTP
- 多吉云
- 华为云 OBS
- Alist
## 4. 能否支持上传视频文件 ## 4. 能否支持上传视频文件
@@ -46,6 +51,8 @@ PicList本体支持了如下图床:
- `阿里云 OSS` - `阿里云 OSS`
- `Imgur` - `Imgur`
- `Webdav` - `Webdav`
- `本地图床`
- `SFTP`
PicList计划整合和优化现有插件,内置更多的常用图床。 PicList计划整合和优化现有插件,内置更多的常用图床。
@@ -97,3 +104,15 @@ macOS: `~/Library/Application\ Support/picgo/assets/simhei.ttf`
## 13. 使用aws-s3插件上传到cloudflare R2时出现上传失败问题 ## 13. 使用aws-s3插件上传到cloudflare R2时出现上传失败问题
R2的endpoint地址会出现被GFW sni阻断的问题,查看piclist.log后将对应的ip地址加入代理列表可解决。 R2的endpoint地址会出现被GFW sni阻断的问题,查看piclist.log后将对应的ip地址加入代理列表可解决。
## 14. PicList兼容所有的PicGo插件吗?
PicList兼容绝大部分的PicGo插件。然而由于PicList使用了更新的electron版本,与旧版本的sharp库不兼容,所以部分插件可能无法使用。
已知的无法使用的插件有:
- picgo-plugin-watermark (已经内置)
- picgo-plugin-pic-migrater (该插件会校验PicGo的版本,无法使用,请换用pic-migrater-piclist插件)
- picgo-plugin-auto-delete (已经内置)
欢迎大家测试其他插件,如果有无法使用的插件,欢迎开issue反馈。
+19
View File
@@ -29,6 +29,11 @@ Currently, the supported image hosting platforms are:
- GitHub - GitHub
- Webdav - Webdav
- Aws S3 - Aws S3
- Local path
- Built-in SFTP
- Doge Cloud
- Huawei Cloud OBS
- Alist
## 4. Is it possible to upload video files? ## 4. Is it possible to upload video files?
@@ -46,6 +51,8 @@ PicList itself supports the following image hosting platforms:
- Aliyun OSS - Aliyun OSS
- Imgur - Imgur
- Webdav - Webdav
- Local path
- SFTP
PicList plans to integrate and optimize existing plugins and embed more commonly used image hosting platforms. PicList plans to integrate and optimize existing plugins and embed more commonly used image hosting platforms.
@@ -104,3 +111,15 @@ The font file download address: [https://release.piclist.cn/simhei.ttf](https://
## 13. Upload failed when using aws-s3 plugin to upload to cloudflare R2 ## 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. 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.
+29 -40
View File
@@ -16,7 +16,7 @@
简体中文 | [English](https://github.com/Kuingsmile/PicList/blob/dev/README_en.md) 简体中文 | [English](https://github.com/Kuingsmile/PicList/blob/dev/README_en.md)
PicList是一款云存储/图床平台管理和文件上传工具,基于PicGo的进行了深度二次开发,保留了PicGo的所有功能的同时,为相册添加了同步云端删除功能,同时增加了完整的云存储管理功能,包括云端目录查看、文件搜索、批量上传下载和删除文件,复制多种格式文件链接和图片/markdown/文本/视频预览等,另外还有更加强大的相册和多项功能新增或优化。 PicList是一款高效的云存储图床平台管理工具,PicGo的基础上经过深度二次开发,不仅完整保留了PicGo的所有功能,还增添了许多新的feature。例如相册支持同步云端删除文件,内置图床额外添加了WebDav、本地图床和SFTP等。PicList同时增加了完整的云存储管理功能,包括云端目录查看、文件搜索、批量上传下载和删除文件,复制多种格式文件链接和图片/markdown/文本/视频预览等,另外还有更加强大的相册和多项功能新增或优化。
## 如何从PicGo迁移 ## 如何从PicGo迁移
@@ -30,16 +30,17 @@ PicList的内核使用的是原版PicGo-Core基础上修改的[PicList-core](htt
## 特色功能 ## 特色功能
- 保留了PicGo的所有功能,兼容已有的PicGo插件系统,包括和typora、obsidian等的搭配 - 保留了PicGo的所有功能,兼容绝大部分已有的PicGo插件,包括和Typora、Obsidian等软件的搭配
- 新增了对webdav上传,imgur账户上传,本地文件夹上传等的支持 - 新增了多个内置图床,如WebDav、本地图床和SFTP等,原内置imgur图床额外支持登录账号上传
- 相册中可同步删除云端图片,同时新增了高级搜索和排序,批量修改URL等功能 - 相册中可同步删除云端图片,支持所有内置图床和多个插件
- 内置水印添加、图片压缩、图片缩放、图片旋转和图片格式转换等功能,支持自定义配置,且可以通过CLI命令行调用 - 相册新增了高级搜索和排序,批量修改URL等功能
- 新增配置多端同步功能 - 内置水印添加、图片压缩、图片缩放、图片旋转和图片格式转换等功能,同时支持高级重命名
- 支持管理所有图床,可以在线进行云端目录查看、文件搜索、批量上传、批量下载、删除文件等 - 支持配置同步至Github或Gitee仓库
- 支持管理十余种图床,可以在线进行云端目录查看、文件搜索、批量上传、批量下载、删除文件等
- 支持预览多种格式的文件,包括图片、视频、纯文本文件和markdown文件等,具体支持的格式请参考[支持的文件格式列表](https://github.com/Kuingsmile/PicList/blob/dev/supported_format.md) - 支持预览多种格式的文件,包括图片、视频、纯文本文件和markdown文件等,具体支持的格式请参考[支持的文件格式列表](https://github.com/Kuingsmile/PicList/blob/dev/supported_format.md)
- 支持正则表达式的批量云端文件重命名 - 支持启用正则表达式的批量云端文件重命名
- 对于私有存储桶等支持复制预签名链接进行分享 - 对于私有存储桶等支持复制预签名链接进行分享
- 支持自动更新,无需每次手动下载,支持多种启动模式选择,还有更多功能细节新增和优化 - 支持软件自动更新,支持多种启动模式,还有更多功能细节新增和优化
- 优化了PicGo的界面,解锁了窗口大小限制,同时美化了部分界面布局 - 优化了PicGo的界面,解锁了窗口大小限制,同时美化了部分界面布局
- mac平台安装包已签名,从源头解决了PicGo上的安装包已损坏的日经问题 - mac平台安装包已签名,从源头解决了PicGo上的安装包已损坏的日经问题
@@ -49,10 +50,7 @@ PicList的内核使用的是原版PicGo-Core基础上修改的[PicList-core](htt
**Typora 1.6.0-dev以及以上版本现在已经原生支持PicList了** **Typora 1.6.0-dev以及以上版本现在已经原生支持PicList了**
下载地址 [下载地址](https://typora.io/releases/all)
[Windows 版本](https://download.typora.io/windows/typora-setup-x64-1.6.0-dev.exe "windows")
[Mac OS版本](https://download.typora.io/mac/Typora-1.6.0-dev.dmg "macOS")
#### 1.6.0版本以下 #### 1.6.0版本以下
@@ -74,7 +72,7 @@ MacOS:
### 如何在Obsidian中使用 ### 如何在Obsidian中使用
在社区插件中搜索安装 `Image auto upload Plugin`,然后进入插件设置页面,修改默认上传器为 `PicGo(app)`,设置 `PicGo server``http://127.0.0.1:36677/upload`即可,如下图所示: 在社区插件中搜索安装 `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) ![image](https://user-images.githubusercontent.com/96409857/226522718-8378c480-9fb4-4785-87e1-d59808862016.png)
@@ -92,10 +90,15 @@ MacOS:
| S3 API兼容平台 | ✔️ | ✔️ | | S3 API兼容平台 | ✔️ | ✔️ |
| WebDAV | ✔️ | ✔️ | | WebDAV | ✔️ | ✔️ |
| 本地文件夹 | ✔️ | ✔️ | | 本地文件夹 | ✔️ | ✔️ |
| 内置SFTP | ✔️ | ✔️ |
| 多吉云 | ✔️ | ✔️ |
| 插件 | 相册云删除 | | 插件 | 相册云删除 |
| :----------------------------------------------------------: | :--------: | | :----------------------------------------------------------------------------------------: | :--------: |
| [picgo-plugin-s3](https://github.com/wayjam/picgo-plugin-s3) | ✔️ | | [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) | ✔️ |
## 下载安装 ## 下载安装
@@ -115,29 +118,14 @@ brew install piclist --cask
brew uninstall piclist brew uninstall piclist
``` ```
### Mac特殊说明
如果macOS系统安装完PicList显示「文件已损坏」或者安装完打开没有反应,请升级到PicList V1.4.1以上版本。
从V1.4.1版本开始,所有的mac安装包均经过了我的开发者证书签名,不会再被macOS系统识别为「恶意软件」,不会再出现「文件已损坏」的提示。
### Mac App Store
由于Mac App Store的沙盒机制,导致多项功能无法正常使用,因此不再支持Mac App Store的安装方式。
如果您已经通过Mac App Store购买了PicList,请添加我的微信 `pku_sq_ma`,我会为您退费。
再次感谢您对PicList的支持。
## 应用截图 ## 应用截图
![image](https://user-images.githubusercontent.com/96409857/222900642-f1d04a41-f025-4f3c-b838-bae770e0b929.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/1b76c0c4-753c-4d66-aa24-f805f9c2da15)
![image](https://user-images.githubusercontent.com/96409857/222900656-6bb33045-6672-4c4d-ac34-1b9ba86011cc.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/56cf838a-a2eb-40af-96d4-1ffea25400af)
![image](https://user-images.githubusercontent.com/96409857/220510112-e524f270-ab56-4e8b-bfb2-eb0a77e559ef.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/bca7688a-e07f-4e80-9edd-c224298fa8ab)
![image](https://user-images.githubusercontent.com/96409857/220510176-8a3f9f19-9182-4b56-b943-fc408ef63f22.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/3e48e03d-b0b2-49e2-92a6-a52e0884677d)
![image](https://user-images.githubusercontent.com/96409857/220510302-f193fc77-db1b-4817-81ff-3ab1c3a1f4d3.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/29de0046-1aef-4b28-95a6-b26c6e297c6f)
![image](https://user-images.githubusercontent.com/96409857/220510371-a2fad42e-8063-4014-a691-ca5b66b8cc60.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/e1c04488-2d3a-4e8f-aa26-ce41d0a383e2)
![image](https://user-images.githubusercontent.com/96409857/220510427-b85ffc0a-55cf-43f1-b1b0-ba7776a75de2.png)
## 微信交流群 ## 微信交流群
@@ -148,8 +136,8 @@ brew uninstall piclist
1. 你需要有 Node、Git 环境,了解 npm 的相关知识。 1. 你需要有 Node、Git 环境,了解 npm 的相关知识。
2. git clone [https://github.com/Kuingsmile/PicList.git](https://github.com/Kuingsmile/PicList.git) 并进入项目。 2. git clone [https://github.com/Kuingsmile/PicList.git](https://github.com/Kuingsmile/PicList.git) 并进入项目。
`yarn` 下载依赖 `yarn` 下载依赖
注意如果你没有yarn,请去 官网 下载安装后再使用。 用 npm install 将导致未知错误! 注意如果你没有`yarn`,请去 官网 下载安装后再使用。 用 `npm install` 将导致未知错误!
3. Mac 需要有 Xcode 环境,Windows 需要有 VS 环境。 3. Mac 需要有 `Xcode` 环境,Windows 需要有 `VS` 环境。
4. 如果需要贡献代码,可以参考[贡献指南](https://github.com/Kuingsmile/PicList/blob/dev/CONTRIBUTING.md)。 4. 如果需要贡献代码,可以参考[贡献指南](https://github.com/Kuingsmile/PicList/blob/dev/CONTRIBUTING.md)。
### 开发模式 ### 开发模式
@@ -158,6 +146,7 @@ brew uninstall piclist
`ctrl+c` # 退出开发模式 `ctrl+c` # 退出开发模式
`yarn run dev` # 重新进入开发模式 `yarn run dev` # 重新进入开发模式
注:Windows 开发模式运行之后会在底部任务栏的右下角应用区出现 PicList 的应用图标。 注:Windows 开发模式运行之后会在底部任务栏的右下角应用区出现 PicList 的应用图标。
### 生产模式 ### 生产模式
+33 -31
View File
@@ -16,7 +16,8 @@
[简体中文](https://github.com/Kuingsmile/PicList/blob/dev/README.md) | English [简体中文](https://github.com/Kuingsmile/PicList/blob/dev/README.md) | English
PicList is a cloud storage platform management and file upload tool based on PicGo, which has been deeply redeveloped. It retains all the functions of PicGo, adds the function of synchronous cloud deletion to the album, and adds a complete cloud storage management function, including cloud directory viewing, file search, batch upload and download, and file deletion, copying multiple formats of file links and image/markdown/text/video preview, etc. Additionally, there are several other feature improvements and additions.
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 ## How to migrate from PicGo
@@ -30,19 +31,19 @@ if you want to use PicList-core, please go to [https://github.com/Kuingsmile/Pic
## Features ## Features
- Maintain all the functions of PicGo, compatible with the existing PicGo plug-in system, including the combination with typora, obsidian and other software - 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.
- Add support for webdav upload, imgur account upload, local path upload, etc. - Added multiple built-in image hosting platforms, such as WebDav, local image hosting, and SFTP. The original built-in imgur image host now also supports account login for uploading.
- Synchronous cloud deletion of pictures in the album, advanced search and sorting features have been added, along with the ability to bulk modify URLs. - Within the album, you can synchronize the deletion of cloud images. This is supported across all built-in image hosts and multiple plugins.
- Built-in watermark addition, image compression, image scaling, image rotation and image format conversion functions, support custom configuration, and can be called through CLI command line - The album now offers advanced search and sorting features, as well as batch URL modification.
- Add the configuration of multi-device synchronization function. - Built-in tools for adding watermarks, compressing images, scaling images, rotating images, and converting image formats are now available. Advanced renaming is also supported.
- Add - Configuration can be synchronized to Github or Gitee repositories.
- Support management of all cloud storage platforms, can be online to view the cloud directory, file search, batch upload, batch download, delete files and other operations - 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) - 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)
- Support batch cloud file renaming based on regular expressions - Supports the use of regular expressions for batch renaming of cloud files.
- The management interface uses the built-in database cache directory to accelerate the directory loading speed - For private storage buckets, pre-signed link copying for sharing is available.
- Support automatic update, no need to download manually every time, support multiple startup mode selection, and more function details are added and optimized - Software auto-updates are available, along with multiple startup modes, and many other feature details have been added and optimized.
- Optimized the PicGo interface, unlocked the window size limit, and beautified the interface layout - The PicGo interface has been enhanced, window size restrictions have been unlocked, and some interface layouts have been beautified.
- The installation package of the mac platform has been signed, and the installation package has been corrupted from the source to solve the daily problem of PicGo's installation package has been corrupted - 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 in Typora ### How to use in Typora
@@ -50,11 +51,7 @@ if you want to use PicList-core, please go to [https://github.com/Kuingsmile/Pic
**Typora 1.6.0-dev and above versions now support PicList natively** **Typora 1.6.0-dev and above versions now support PicList natively**
download link: [download link](https://typora.io/releases/all)
[Windows Version](https://download.typora.io/windows/typora-setup-x64-1.6.0-dev.exe)
[Mac OS Version](https://download.typora.io/mac/Typora-1.6.0-dev.dmg)
#### **Version < 1.6.0-dev** #### **Version < 1.6.0-dev**
@@ -76,7 +73,7 @@ The verification of the upload option may have problems, you can ignore it, and
### How to use in Obsidian ### How to use in Obsidian
Search and install `Image auto upload Plugin` in the community plugin, then enter the plugin settings page, modify the default uploader to `PicGo(app)`, set `PicGo server` to `http://127.0.0.1:36677/upload`, as shown below: 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) ![image](https://user-images.githubusercontent.com/96409857/226522718-8378c480-9fb4-4785-87e1-d59808862016.png)
@@ -94,10 +91,15 @@ Search and install `Image auto upload Plugin` in the community plugin, then ente
| S3 API compatible platform | ✔️ | ✔️ | | S3 API compatible platform | ✔️ | ✔️ |
| WebDAV | ✔️ | ✔️ | | WebDAV | ✔️ | ✔️ |
| Local | ✔️ | ✔️ | | Local | ✔️ | ✔️ |
| Built-in SFTP | ✔️ | ✔️ |
| Doge Cloud | ✔️ | ✔️ |
| Plugin | Album cloud deletion | | Plugin | Album cloud deletion |
| :----------------------------------------------------------: | :------------------: | | :----------------------------------------------------------------------------------------: | :------------------: |
| [picgo-plugin-s3](https://github.com/wayjam/picgo-plugin-s3) | ✔️ | | [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 ## Download and install
@@ -135,13 +137,12 @@ Thank you again for your support for PicList.
## Application screenshot ## Application screenshot
![image](https://user-images.githubusercontent.com/96409857/222900642-f1d04a41-f025-4f3c-b838-bae770e0b929.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/1b76c0c4-753c-4d66-aa24-f805f9c2da15)
![image](https://user-images.githubusercontent.com/96409857/222900656-6bb33045-6672-4c4d-ac34-1b9ba86011cc.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/56cf838a-a2eb-40af-96d4-1ffea25400af)
![image](https://user-images.githubusercontent.com/96409857/220510112-e524f270-ab56-4e8b-bfb2-eb0a77e559ef.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/bca7688a-e07f-4e80-9edd-c224298fa8ab)
![image](https://user-images.githubusercontent.com/96409857/220510176-8a3f9f19-9182-4b56-b943-fc408ef63f22.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/3e48e03d-b0b2-49e2-92a6-a52e0884677d)
![image](https://user-images.githubusercontent.com/96409857/220510302-f193fc77-db1b-4817-81ff-3ab1c3a1f4d3.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/29de0046-1aef-4b28-95a6-b26c6e297c6f)
![image](https://user-images.githubusercontent.com/96409857/220510371-a2fad42e-8063-4014-a691-ca5b66b8cc60.png) ![image](https://github.com/Kuingsmile/PicList/assets/96409857/e1c04488-2d3a-4e8f-aa26-ce41d0a383e2)
![image](https://user-images.githubusercontent.com/96409857/220510427-b85ffc0a-55cf-43f1-b1b0-ba7776a75de2.png)
## WeChat group ## WeChat group
@@ -152,8 +153,8 @@ Thank you again for your support for PicList.
1. You need to have Node, Git environment, and understand the related knowledge of npm. 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. 2. git clone [https://github.com/Kuingsmile/PicList.git](https://github.com/Kuingsmile/PicList.git) and enter the project.
`yarn` download dependencies `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! 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. 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) 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 ### Development mode
@@ -162,6 +163,7 @@ Enter `yarn run dev` to enter development mode, which has hot reload feature. Ho
`ctrl+c` # Exit development mode `ctrl+c` # Exit development mode
`yarn run dev` # Re-enter 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. 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 ### Production mode
+14 -10
View File
@@ -5,7 +5,7 @@
"name": "Kuingsmile", "name": "Kuingsmile",
"email": "pkukuing@gmail.com" "email": "pkukuing@gmail.com"
}, },
"description": "PicList is a simple and powerful cloude storage manage tool.", "description": "A powerful cloude storage manage tool.",
"homepage": "https://piclist.cn", "homepage": "https://piclist.cn",
"bugs": { "bugs": {
"url": "https://github.com/Kuingsmile/PicList/issues", "url": "https://github.com/Kuingsmile/PicList/issues",
@@ -32,26 +32,27 @@
"sha256": "node ./scripts/gen-sha256.js" "sha256": "node ./scripts/gen-sha256.js"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.272.0", "@aws-sdk/client-s3": "^3.388.0",
"@aws-sdk/lib-storage": "^3.272.0", "@aws-sdk/lib-storage": "^3.388.0",
"@aws-sdk/s3-request-presigner": "^3.272.0", "@aws-sdk/s3-request-presigner": "^3.388.0",
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"@highlightjs/vue-plugin": "^2.1.0", "@highlightjs/vue-plugin": "^2.1.0",
"@nodelib/fs.walk": "^2.0.0",
"@octokit/rest": "^19.0.7", "@octokit/rest": "^19.0.7",
"@picgo/i18n": "^1.0.0", "@picgo/i18n": "^1.0.0",
"@picgo/store": "^2.0.4", "@picgo/store": "^2.0.4",
"@smithy/node-http-handler": "^2.0.2",
"@types/marked": "^4.0.8", "@types/marked": "^4.0.8",
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",
"@videojs-player/vue": "^1.0.0", "@videojs-player/vue": "^1.0.0",
"ali-oss": "^6.17.1", "ali-oss": "^6.18.0",
"aws-sdk": "^2.1373.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"compare-versions": "^4.1.3", "compare-versions": "^4.1.3",
"core-js": "^3.27.1", "core-js": "^3.27.1",
"cos-nodejs-sdk-v5": "^2.12.1", "cos-nodejs-sdk-v5": "^2.12.4",
"dexie": "^3.2.4", "dexie": "^3.2.4",
"electron-updater": "^6.1.1", "electron-updater": "^6.1.1",
"element-plus": "^2.3.8", "element-plus": "^2.3.9",
"epipebomb": "^1.0.0", "epipebomb": "^1.0.0",
"fast-xml-parser": "^4.2.5", "fast-xml-parser": "^4.2.5",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@@ -65,12 +66,14 @@
"marked": "^4.3.0", "marked": "^4.3.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"node-ssh-no-cpu-features": "^1.0.1",
"nodejs-file-downloader": "^4.12.1", "nodejs-file-downloader": "^4.12.1",
"piclist": "^0.8.5", "piclist": "^0.8.12",
"pinia": "^2.1.4", "pinia": "^2.1.4",
"pinia-plugin-persistedstate": "^3.1.0", "pinia-plugin-persistedstate": "^3.1.0",
"qiniu": "^7.8.0", "qiniu": "^7.9.0",
"qrcode.vue": "^3.4.0", "qrcode.vue": "^3.4.0",
"querystring": "^0.2.1",
"shell-path": "2.1.0", "shell-path": "2.1.0",
"upyun": "^3.4.6", "upyun": "^3.4.6",
"uuid": "^9.0.0", "uuid": "^9.0.0",
@@ -120,6 +123,7 @@
"eslint-plugin-promise": "^5.1.0", "eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^9.9.0", "eslint-plugin-vue": "^9.9.0",
"husky": "^3.1.0", "husky": "^3.1.0",
"node-loader": "^2.0.0",
"stylus": "^0.54.7", "stylus": "^0.54.7",
"stylus-loader": "^3.0.2", "stylus-loader": "^3.0.2",
"typescript": "^4.9.5", "typescript": "^4.9.5",
+63
View File
@@ -275,6 +275,7 @@ SETTINGS_SYNC_DOWNLOAD_FAILED: Download failed
SETTINGS_SYNC_COMMON_CONFIG: Common configuration SETTINGS_SYNC_COMMON_CONFIG: Common configuration
SETTINGS_SYNC_MANAGE_CONFIG: Manage configuration SETTINGS_SYNC_MANAGE_CONFIG: Manage configuration
SETTINGS_AUTO_IMPORT: Auto import config in manage page SETTINGS_AUTO_IMPORT: Auto import config in manage page
SETTINGS_AUTO_IMPORT_SELECT_PICBED: Select picbed
SETTINGS_TAB_SYSTEM: System SETTINGS_TAB_SYSTEM: System
SETTINGS_TAB_SYNC_CONFIG: Sync and Configuration SETTINGS_TAB_SYNC_CONFIG: Sync and Configuration
SETTINGS_TAB_UPLOAD: Upload SETTINGS_TAB_UPLOAD: Upload
@@ -322,6 +323,7 @@ PLUGIN_INSTALLED: Installed
PLUGIN_DOING_SOMETHING: Doing... PLUGIN_DOING_SOMETHING: Doing...
PLUGIN_LIST: Plugin List PLUGIN_LIST: Plugin List
PLUGIN_IMPORT_LOCAL: Import Local Plugins PLUGIN_IMPORT_LOCAL: Import Local Plugins
PLUGIN_UPDATE_ALL: Update All Plugins
# tips # tips
@@ -574,6 +576,8 @@ MANAGE_CONSTANT_S3_BUCKET_DESC: Bucket name - Optional
MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: English comma-separated list, e.g. bucket1,bucket2 MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: English comma-separated list, e.g. bucket1,bucket2
MANAGE_CONSTANT_S3_BASE_DIR_DESC: Base directory - Optional MANAGE_CONSTANT_S3_BASE_DIR_DESC: Base directory - Optional
MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: English comma-separated list, e.g. /dir1,/dir2 MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: English comma-separated list, e.g. /dir1,/dir2
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_DESC: Enable Doge Cloud API
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_TOOLTIP: Support Doge Cloud API
MANAGE_CONSTANT_S3_PAGING_DESC: Enable pagination MANAGE_CONSTANT_S3_PAGING_DESC: Enable pagination
MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: Items per page MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: Items per page
MANAGE_CONSTANT_S3_EXPLAIN: When configuring bucket name and base directory, they can be set using English comma separation. The order must be consistent and missing or empty items will use the default value. MANAGE_CONSTANT_S3_EXPLAIN: When configuring bucket name and base directory, they can be set using English comma separation. The order must be consistent and missing or empty items will use the default value.
@@ -598,6 +602,9 @@ MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_DESC: Custom Domain - Optional
MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_PLACEHOLDER: 'e.g. https://example.com' MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_PLACEHOLDER: 'e.g. https://example.com'
MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_TOOLTIP: If your WebDAV server supports custom domains, please fill in MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_TOOLTIP: If your WebDAV server supports custom domains, please fill in
MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_RULE_MESSAGE: 'Custom domain should start with http:// or https://' MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_RULE_MESSAGE: 'Custom domain should start with http:// or https://'
MANAGE_CONSTANT_WEBDAV_WEB_PATH: Web Path - Optional
MANAGE_CONSTANT_WEBDAV_WEB_PATH_PLACEHOLDER: 'e.g. test/ttc'
MANAGE_CONSTANT_WEBDAV_WEB_PATH_TOOLTIP: 'Used to generate URL'
MANAGE_CONSTANT_WEBDAV_PROXY_DESC: Proxy - Optional MANAGE_CONSTANT_WEBDAV_PROXY_DESC: Proxy - Optional
MANAGE_CONSTANT_WEBDAV_PROXY_PLACEHOLDER: 'e.g. http://127.0.0.1:1080' MANAGE_CONSTANT_WEBDAV_PROXY_PLACEHOLDER: 'e.g. http://127.0.0.1:1080'
MANAGE_CONSTANT_WEBDAV_PROXY_TOOLTIP: If special network environment is required to access, please use proxy MANAGE_CONSTANT_WEBDAV_PROXY_TOOLTIP: If special network environment is required to access, please use proxy
@@ -606,6 +613,61 @@ MANAGE_CONSTANT_WEBDAV_SSL_TOOLTIP: Depending on the configuration of your WebDA
MANAGE_CONSTANT_WEBDAV_EXPLAIN: 'WebDAV Configuration' MANAGE_CONSTANT_WEBDAV_EXPLAIN: 'WebDAV Configuration'
MANAGE_CONSTANT_WEBDAV_REFER_TEXT: 'Refer to:' MANAGE_CONSTANT_WEBDAV_REFER_TEXT: 'Refer to:'
MANAGE_CONSTANT_LOCAL_NAME: Local
MANAGE_CONSTANT_LOCAL_ALIAS_DESC: Alias - Required
MANAGE_CONSTANT_LOCAL_ALIAS_PLACEHOLDER: Unique identifier for this configuration
MANAGE_CONSTANT_LOCAL_BASE_DIR_DESC: Base directory - Required
MANAGE_CONSTANT_LOCAL_BASE_DIR_PLACEHOLDER: 'e.g. /dir1'
MANAGE_CONSTANT_LOCAL_CUSTOM_URL_DESC: Custom Domain - Optional
MANAGE_CONSTANT_LOCAL_CUSTOM_URL_PLACEHOLDER: 'e.g. https://example.com'
MANAGE_CONSTANT_LOCAL_CUSTOM_URL_TOOLTIP: If your local path supports custom domains, please fill in
MANAGE_CONSTANT_LOCAL_WEB_PATH: Web Path - Optional
MANAGE_CONSTANT_LOCAL_WEB_PATH_PLACEHOLDER: 'e.g. test/ttc'
MANAGE_CONSTANT_LOCAL_WEB_PATH_TOOLTIP: 'Used to generate URL'
MANAGE_CONSTANT_LOCAL_EXPLAIN: 'Local Configuration'
MANAGE_CONSTANT_LOCAL_REFER_TEXT: 'Refer to:'
MANAGE_CONSTANT_LOCAL_BASE_DIR_RULE_MESSAGE: baseDir cannot be empty
MANAGE_CONSTANT_LOCAL_BUCKET_DESC: Special Configuration
MANAGE_CONSTANT_LOCAL_BUCKET_PLACEHOLDER: 'bucket1'
MANAGE_CONSTANT_LOCAL_BUCKET_TOOLTIP: This cannot be modified, only for software compatibility consideration
MANAGE_CONSTANT_SFTP_NAME: SFTP
MANAGE_CONSTANT_SFTP_ALIAS_DESC: Alias - Required
MANAGE_CONSTANT_SFTP_ALIAS_PLACEHOLDER: Unique identifier for this configuration
MANAGE_CONSTANT_SFTP_HOST_DESC: SSH Host - Required
MANAGE_CONSTANT_SFTP_HOST_PLACEHOLDER: 'e.g. 233.233.233.233'
MANAGE_CONSTANT_SFTP_PORT_DESC: SSH Port - Required
MANAGE_CONSTANT_SFTP_PORT_PLACEHOLDER: 'e.g. 22'
MANAGE_CONSTANT_SFTP_USERNAME_DESC: Username
MANAGE_CONSTANT_SFTP_USERNAME_PLACEHOLDER: Please enter your username
MANAGE_CONSTANT_SFTP_PASSWORD_DESC: Password
MANAGE_CONSTANT_SFTP_PASSWORD_PLACEHOLDER: Please enter your password
MANAGE_CONSTANT_SFTP_PRIVATE_KEY_DESC: Private Key
MANAGE_CONSTANT_SFTP_PRIVATE_KEY_PLACEHOLDER: Please enter your private key
MANAGE_CONSTANT_SFTP_PRIVATE_KEY_TOOLTIP: 'e.g. /home/user/.ssh/id_rsa'
MANAGE_CONSTANT_SFTP_PASSPHRASE_DESC: Private Key Password
MANAGE_CONSTANT_SFTP_PASSPHRASE_PLACEHOLDER: Please enter your private key password
MANAGE_CONSTANT_SFTP_BASE_DIR_DESC: Base Directory
MANAGE_CONSTANT_SFTP_BASE_DIR_PLACEHOLDER: 'e.g. /dir1'
MANAGE_CONSTANT_SFTP_CUSTOM_URL_DESC: Custom Domain
MANAGE_CONSTANT_SFTP_CUSTOM_URL_PLACEHOLDER: 'e.g. https://example.com'
MANAGE_CONSTANT_SFTP_CUSTOM_URL_TOOLTIP: If your SFTP server supports custom domains, please fill in
MANAGE_CONSTANT_SFTP_WEB_PATH: Web Path
MANAGE_CONSTANT_SFTP_WEB_PATH_PLACEHOLDER: 'e.g. test/ttc'
MANAGE_CONSTANT_SFTP_WEB_PATH_TOOLTIP: 'Used to generate URL'
MANAGE_CONSTANT_SFTP_FILE_PERMISSIONS_DESC: File mode
MANAGE_CONSTANT_SFTP_FILE_PERMISSIONS_PLACEHOLDER: 'e.g. 0644'
MANAGE_CONSTANT_SFTP_FILE_PERMISSIONS_TOOLTIP: 'Used to set the permissions of uploaded files'
MANAGE_CONSTANT_SFTP_DIR_PERMISSIONS_DESC: Directory mode
MANAGE_CONSTANT_SFTP_DIR_PERMISSIONS_PLACEHOLDER: 'e.g. 0755'
MANAGE_CONSTANT_SFTP_DIR_PERMISSIONS_TOOLTIP: 'Used to set the permissions of uploaded directories'
MANAGE_CONSTANT_SFTP_EXPLAIN: 'SFTP Configuration'
MANAGE_CONSTANT_SFTP_REFER_TEXT: 'Refer to:'
MANAGE_CONSTANT_SFTP_BASE_DIR_RULE_MESSAGE: baseDir cannot be empty
MANAGE_CONSTANT_SFTP_BUCKET_DESC: Special Configuration
MANAGE_CONSTANT_SFTP_BUCKET_PLACEHOLDER: 'e.g. bucket1'
MANAGE_CONSTANT_SFTP_BUCKET_TOOLTIP: This cannot be modified, only for software compatibility consideration
MANAGE_LOGIN_PAGE_PANE_NAME: Saved Config MANAGE_LOGIN_PAGE_PANE_NAME: Saved Config
MANAGE_LOGIN_PAGE_PANE_DESC: Click on the icon or alias to view details, Enter to view the file page, Delete to remove the configuration MANAGE_LOGIN_PAGE_PANE_DESC: Click on the icon or alias to view details, Enter to view the file page, Delete to remove the configuration
MANAGE_LOGIN_PAGE_PANE_LOADING: Importing... MANAGE_LOGIN_PAGE_PANE_LOADING: Importing...
@@ -782,6 +844,7 @@ MANAGE_BUCKET_DELETE_SUCCESS: Deletion successful
MANAGE_BUCKET_DELETE_FAIL: Deletion failed MANAGE_BUCKET_DELETE_FAIL: Deletion failed
MANAGE_BUCKET_DELETE_CANCEL: Deletion has been cancelled MANAGE_BUCKET_DELETE_CANCEL: Deletion has been cancelled
MANAGE_BUCKET_RENAME_INFO_MSG: The new file name is the same as the original file name, no need to rename MANAGE_BUCKET_RENAME_INFO_MSG: The new file name is the same as the original file name, no need to rename
MANAGE_BUCKET_RENAME_SUCCESS: Rename successful
MANAGE_BUCKET_RENAME_ERROR_MSG: Rename failed MANAGE_BUCKET_RENAME_ERROR_MSG: Rename failed
MANAGE_BUCKET_DOWNLOAD_COLUMN_FILENAME: File name MANAGE_BUCKET_DOWNLOAD_COLUMN_FILENAME: File name
MANAGE_BUCKET_DOWNLOAD_COLUMN_FINISHTIME: Completion time MANAGE_BUCKET_DOWNLOAD_COLUMN_FINISHTIME: Completion time
+64
View File
@@ -278,6 +278,7 @@ SETTINGS_SYNC_DOWNLOAD_FAILED: 下载失败
SETTINGS_SYNC_COMMON_CONFIG: 通用配置 SETTINGS_SYNC_COMMON_CONFIG: 通用配置
SETTINGS_SYNC_MANAGE_CONFIG: 管理配置 SETTINGS_SYNC_MANAGE_CONFIG: 管理配置
SETTINGS_AUTO_IMPORT: 管理页面自动导入配置 SETTINGS_AUTO_IMPORT: 管理页面自动导入配置
SETTINGS_AUTO_IMPORT_SELECT_PICBED: 选择需要开启自动导入的图床
SETTINGS_TAB_SYSTEM: 系统设置 SETTINGS_TAB_SYSTEM: 系统设置
SETTINGS_TAB_SYNC_CONFIG: 同步与配置 SETTINGS_TAB_SYNC_CONFIG: 同步与配置
SETTINGS_TAB_UPLOAD: 上传设置 SETTINGS_TAB_UPLOAD: 上传设置
@@ -324,6 +325,7 @@ PLUGIN_INSTALLED: 已安装
PLUGIN_DOING_SOMETHING: 进行中 PLUGIN_DOING_SOMETHING: 进行中
PLUGIN_LIST: 插件列表 PLUGIN_LIST: 插件列表
PLUGIN_IMPORT_LOCAL: 导入本地插件 PLUGIN_IMPORT_LOCAL: 导入本地插件
PLUGIN_UPDATE_ALL: 更新全部插件
# tips # tips
@@ -577,6 +579,8 @@ MANAGE_CONSTANT_S3_BUCKET_DESC: 存储桶名-可选
MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: 英文逗号分隔,例如:bucket1,bucket2 MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: 英文逗号分隔,例如:bucket1,bucket2
MANAGE_CONSTANT_S3_BASE_DIR_DESC: 起始目录-可选 MANAGE_CONSTANT_S3_BASE_DIR_DESC: 起始目录-可选
MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: '英文逗号分隔,例如:/dir1,/dir2' MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: '英文逗号分隔,例如:/dir1,/dir2'
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_DESC: 是否使用Doge Cloud
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_TOOLTIP: 开启后,将使用Doge Cloud的API
MANAGE_CONSTANT_S3_PAGING_DESC: 是否开启分页 MANAGE_CONSTANT_S3_PAGING_DESC: 是否开启分页
MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: 每页显示数量 MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: 每页显示数量
MANAGE_CONSTANT_S3_EXPLAIN: 存储桶名和起始目录配置时可通过英文逗号分隔不同存储桶的设置,顺序必须一致,逗号间留空或缺失项使用默认值 MANAGE_CONSTANT_S3_EXPLAIN: 存储桶名和起始目录配置时可通过英文逗号分隔不同存储桶的设置,顺序必须一致,逗号间留空或缺失项使用默认值
@@ -601,6 +605,9 @@ MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_DESC: 自定义域名-可选
MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_PLACEHOLDER: '例如:https://example.com' MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_PLACEHOLDER: '例如:https://example.com'
MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_TOOLTIP: 如果您的WebDAV服务器支持自定义域名,请填写 MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_TOOLTIP: 如果您的WebDAV服务器支持自定义域名,请填写
MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_RULE_MESSAGE: '自定义域名请以http://或https://开头' MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_RULE_MESSAGE: '自定义域名请以http://或https://开头'
MANAGE_CONSTANT_WEBDAV_WEB_PATH: 网址拼接用起始路径
MANAGE_CONSTANT_WEBDAV_WEB_PATH_PLACEHOLDER: '例如:test/ttc'
MANAGE_CONSTANT_WEBDAV_WEB_PATH_TOOLTIP: '用于拼接网址'
MANAGE_CONSTANT_WEBDAV_PROXY_DESC: 代理-可选 MANAGE_CONSTANT_WEBDAV_PROXY_DESC: 代理-可选
MANAGE_CONSTANT_WEBDAV_PROXY_PLACEHOLDER: '例如:http://127.0.0.1:1080' MANAGE_CONSTANT_WEBDAV_PROXY_PLACEHOLDER: '例如:http://127.0.0.1:1080'
MANAGE_CONSTANT_WEBDAV_PROXY_TOOLTIP: 如果需要特殊网络环境才能访问,请使用代理 MANAGE_CONSTANT_WEBDAV_PROXY_TOOLTIP: 如果需要特殊网络环境才能访问,请使用代理
@@ -609,6 +616,62 @@ MANAGE_CONSTANT_WEBDAV_SSL_TOOLTIP: 根据WebDAV服务器的配置,如果您
MANAGE_CONSTANT_WEBDAV_EXPLAIN: 'WebDAV配置' MANAGE_CONSTANT_WEBDAV_EXPLAIN: 'WebDAV配置'
MANAGE_CONSTANT_WEBDAV_REFER_TEXT: '配置教程请参考: ' MANAGE_CONSTANT_WEBDAV_REFER_TEXT: '配置教程请参考: '
MANAGE_CONSTANT_LOCAL_NAME: 本地存储
MANAGE_CONSTANT_LOCAL_ALIAS_DESC: 配置别名-必需
MANAGE_CONSTANT_LOCAL_ALIAS_PLACEHOLDER: 该配置的唯一标识
MANAGE_CONSTANT_LOCAL_BASE_DIR_DESC: 起始目录-必需
MANAGE_CONSTANT_LOCAL_BASE_DIR_PLACEHOLDER: 请填写起始目录
MANAGE_CONSTANT_LOCAL_CUSTOM_URL_DESC: 自定义域名-可选
MANAGE_CONSTANT_LOCAL_CUSTOM_URL_PLACEHOLDER: '例如:https://example.com'
MANAGE_CONSTANT_LOCAL_CUSTOM_URL_TOOLTIP: 如果您的本地存储支持自定义域名,请填写
MANAGE_CONSTANT_LOCAL_WEB_PATH: 网址拼接用起始路径
MANAGE_CONSTANT_LOCAL_WEB_PATH_PLACEHOLDER: '例如:test/ttc'
MANAGE_CONSTANT_LOCAL_WEB_PATH_TOOLTIP: '用于拼接网址'
MANAGE_CONSTANT_LOCAL_EXPLAIN: '本地存储配置'
MANAGE_CONSTANT_LOCAL_REFER_TEXT: '配置教程请参考: '
MANAGE_CONSTANT_LOCAL_BASE_DIR_RULE_MESSAGE: 起始目录不能为空
MANAGE_CONSTANT_LOCAL_BUCKET_DESC: 特殊配置
MANAGE_CONSTANT_LOCAL_BUCKET_PLACEHOLDER: '例如:bucket1'
MANAGE_CONSTANT_LOCAL_BUCKET_TOOLTIP: 此处不可修改,仅为软件兼容性考虑
MANAGE_CONSTANT_SFTP_NAME: SFTP
MANAGE_CONSTANT_SFTP_ALIAS_DESC: 配置别名-必需
MANAGE_CONSTANT_SFTP_ALIAS_PLACEHOLDER: 该配置的唯一标识
MANAGE_CONSTANT_SFTP_HOST_DESC: SSH地址-必需
MANAGE_CONSTANT_SFTP_HOST_PLACEHOLDER: '例如:233.233.233.233'
MANAGE_CONSTANT_SFTP_PORT_DESC: SSH端口-必需
MANAGE_CONSTANT_SFTP_PORT_PLACEHOLDER: '例如:22'
MANAGE_CONSTANT_SFTP_USERNAME_DESC: 用户名
MANAGE_CONSTANT_SFTP_USERNAME_PLACEHOLDER: 请填写用户名
MANAGE_CONSTANT_SFTP_PASSWORD_DESC: 密码
MANAGE_CONSTANT_SFTP_PASSWORD_PLACEHOLDER: 请填写密码
MANAGE_CONSTANT_SFTP_PRIVATE_KEY_DESC: 私钥地址
MANAGE_CONSTANT_SFTP_PRIVATE_KEY_PLACEHOLDER: 请填写私钥地址
MANAGE_CONSTANT_SFTP_PRIVATE_KEY_TOOLTIP: '例如:/home/user/.ssh/id_rsa'
MANAGE_CONSTANT_SFTP_PASSPHRASE_DESC: 私钥密码
MANAGE_CONSTANT_SFTP_PASSPHRASE_PLACEHOLDER: 请填写私钥密码
MANAGE_CONSTANT_SFTP_BASE_DIR_DESC: 起始目录-可选
MANAGE_CONSTANT_SFTP_BASE_DIR_PLACEHOLDER: '例如:/dir1'
MANAGE_CONSTANT_SFTP_CUSTOM_URL_DESC: 自定义域名-可选
MANAGE_CONSTANT_SFTP_CUSTOM_URL_PLACEHOLDER: '例如:https://example.com'
MANAGE_CONSTANT_SFTP_CUSTOM_URL_TOOLTIP: 如果您的对应路径支持域名访问,请填写
MANAGE_CONSTANT_SFTP_WEB_PATH: 网址拼接用起始路径
MANAGE_CONSTANT_SFTP_WEB_PATH_PLACEHOLDER: '例如:test/ttc'
MANAGE_CONSTANT_SFTP_WEB_PATH_TOOLTIP: '用于拼接网址'
MANAGE_CONSTANT_SFTP_FILE_PERMISSIONS_DESC: 文件权限
MANAGE_CONSTANT_SFTP_FILE_PERMISSIONS_PLACEHOLDER: '例如:0644'
MANAGE_CONSTANT_SFTP_FILE_PERMISSIONS_TOOLTIP: '用于设置上传文件的权限'
MANAGE_CONSTANT_SFTP_DIR_PERMISSIONS_DESC: 目录权限
MANAGE_CONSTANT_SFTP_DIR_PERMISSIONS_PLACEHOLDER: '例如:0755'
MANAGE_CONSTANT_SFTP_DIR_PERMISSIONS_TOOLTIP: '用于设置上传目录的权限'
MANAGE_CONSTANT_SFTP_EXPLAIN: 'SFTP配置'
MANAGE_CONSTANT_SFTP_REFER_TEXT: '配置教程请参考: '
MANAGE_CONSTANT_SFTP_BASE_DIR_RULE_MESSAGE: 起始目录不能为空
MANAGE_CONSTANT_SFTP_BUCKET_DESC: 特殊配置
MANAGE_CONSTANT_SFTP_BUCKET_PLACEHOLDER: '例如:bucket1'
MANAGE_CONSTANT_SFTP_BUCKET_TOOLTIP: 此处不可修改,仅为软件兼容性考虑
MANAGE_LOGIN_PAGE_PANE_NAME: 已保存配置 MANAGE_LOGIN_PAGE_PANE_NAME: 已保存配置
MANAGE_LOGIN_PAGE_PANE_DESC: 点击图标和别名可查看详情,点击进入可查看文件页面,点击删除可删除配置 MANAGE_LOGIN_PAGE_PANE_DESC: 点击图标和别名可查看详情,点击进入可查看文件页面,点击删除可删除配置
MANAGE_LOGIN_PAGE_PANE_LOADING: 导入配置... MANAGE_LOGIN_PAGE_PANE_LOADING: 导入配置...
@@ -785,6 +848,7 @@ MANAGE_BUCKET_DELETE_SUCCESS: 删除成功
MANAGE_BUCKET_DELETE_FAIL: 删除失败 MANAGE_BUCKET_DELETE_FAIL: 删除失败
MANAGE_BUCKET_DELETE_CANCEL: 已取消删除 MANAGE_BUCKET_DELETE_CANCEL: 已取消删除
MANAGE_BUCKET_RENAME_INFO_MSG: 新文件名与原文件名相同,无需重命名 MANAGE_BUCKET_RENAME_INFO_MSG: 新文件名与原文件名相同,无需重命名
MANAGE_BUCKET_RENAME_SUCCESS: 重命名成功
MANAGE_BUCKET_RENAME_ERROR_MSG: 重命名失败 MANAGE_BUCKET_RENAME_ERROR_MSG: 重命名失败
MANAGE_BUCKET_DOWNLOAD_COLUMN_FILENAME: 文件名 MANAGE_BUCKET_DOWNLOAD_COLUMN_FILENAME: 文件名
MANAGE_BUCKET_DOWNLOAD_COLUMN_FINISHTIME: 完成时间 MANAGE_BUCKET_DOWNLOAD_COLUMN_FINISHTIME: 完成时间
+63
View File
@@ -276,6 +276,7 @@ SETTINGS_SYNC_DOWNLOAD_FAILED: 下載失敗
SETTINGS_SYNC_COMMON_CONFIG: 通用配置 SETTINGS_SYNC_COMMON_CONFIG: 通用配置
SETTINGS_SYNC_MANAGE_CONFIG: 管理配置 SETTINGS_SYNC_MANAGE_CONFIG: 管理配置
SETTINGS_AUTO_IMPORT: 管理頁面自動導入配置 SETTINGS_AUTO_IMPORT: 管理頁面自動導入配置
SETTINGS_AUTO_IMPORT_SELECT_PICBED: 選擇需要開啟自動導入的圖床
SETTINGS_TAB_SYSTEM: 系統設置 SETTINGS_TAB_SYSTEM: 系統設置
SETTINGS_TAB_SYNC_CONFIG: 同步與配置 SETTINGS_TAB_SYNC_CONFIG: 同步與配置
SETTINGS_TAB_UPLOAD: 上傳設置 SETTINGS_TAB_UPLOAD: 上傳設置
@@ -322,6 +323,7 @@ PLUGIN_INSTALLED: 已安裝
PLUGIN_DOING_SOMETHING: 進行中 PLUGIN_DOING_SOMETHING: 進行中
PLUGIN_LIST: 插件列表 PLUGIN_LIST: 插件列表
PLUGIN_IMPORT_LOCAL: 導入本地插件 PLUGIN_IMPORT_LOCAL: 導入本地插件
PLUGIN_UPDATE_ALL: 更新全部插件
# tips # tips
@@ -574,6 +576,8 @@ MANAGE_CONSTANT_S3_BUCKET_DESC: 存儲桶名-可選
MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: 英文逗號分隔,例如:bucket1,bucket2 MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: 英文逗號分隔,例如:bucket1,bucket2
MANAGE_CONSTANT_S3_BASE_DIR_DESC: 起始目錄-可選 MANAGE_CONSTANT_S3_BASE_DIR_DESC: 起始目錄-可選
MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: '英文逗號分隔,例如:/dir1,/dir2' MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: '英文逗號分隔,例如:/dir1,/dir2'
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_DESC: 啟用 Doge Cloud 支援
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_TOOLTIP: 啟用後,將會啟用Doge Cloud API
MANAGE_CONSTANT_S3_PAGING_DESC: 是否開啟分頁 MANAGE_CONSTANT_S3_PAGING_DESC: 是否開啟分頁
MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: 每頁顯示數量 MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: 每頁顯示數量
MANAGE_CONSTANT_S3_EXPLAIN: 存儲桶名和起始目錄配置時可通過英文逗號分隔不同存儲桶的設置,順序必須一致,逗號間留空或缺失項使用默認值 MANAGE_CONSTANT_S3_EXPLAIN: 存儲桶名和起始目錄配置時可通過英文逗號分隔不同存儲桶的設置,順序必須一致,逗號間留空或缺失項使用默認值
@@ -598,6 +602,9 @@ MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_DESC: 自訂網域名稱-可選
MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_PLACEHOLDER: '例如:https://example.com' MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_PLACEHOLDER: '例如:https://example.com'
MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_TOOLTIP: 如果您的WebDAV伺服器支援自訂網域名稱,請填寫 MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_TOOLTIP: 如果您的WebDAV伺服器支援自訂網域名稱,請填寫
MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_RULE_MESSAGE: '自訂網域名稱請以http://或https://開頭' MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_RULE_MESSAGE: '自訂網域名稱請以http://或https://開頭'
MANAGE_CONSTANT_WEBDAV_WEB_PATH: WebDAV網路路徑-可選
MANAGE_CONSTANT_WEBDAV_WEB_PATH_PLACEHOLDER: '例如:test/ttc'
MANAGE_CONSTANT_WEBDAV_WEB_PATH_TOOLTIP: '用於拼接網址'
MANAGE_CONSTANT_WEBDAV_PROXY_DESC: 代理-可選 MANAGE_CONSTANT_WEBDAV_PROXY_DESC: 代理-可選
MANAGE_CONSTANT_WEBDAV_PROXY_PLACEHOLDER: '例如:http://127.0.0.1:1080' MANAGE_CONSTANT_WEBDAV_PROXY_PLACEHOLDER: '例如:http://127.0.0.1:1080'
MANAGE_CONSTANT_WEBDAV_PROXY_TOOLTIP: 如果需要特殊網路環境才能訪問,請使用代理 MANAGE_CONSTANT_WEBDAV_PROXY_TOOLTIP: 如果需要特殊網路環境才能訪問,請使用代理
@@ -606,6 +613,61 @@ MANAGE_CONSTANT_WEBDAV_SSL_TOOLTIP: 根據WebDAV伺服器的配置,如果您
MANAGE_CONSTANT_WEBDAV_EXPLAIN: 'WebDAV配置' MANAGE_CONSTANT_WEBDAV_EXPLAIN: 'WebDAV配置'
MANAGE_CONSTANT_WEBDAV_REFER_TEXT: '配置教程請參考: ' MANAGE_CONSTANT_WEBDAV_REFER_TEXT: '配置教程請參考: '
MANAGE_CONSTANT_LOCAL_NAME: 本地
MANAGE_CONSTANT_LOCAL_ALIAS_DESC: 配置別名-必需
MANAGE_CONSTANT_LOCAL_ALIAS_PLACEHOLDER: 該配置的唯一標識
MANAGE_CONSTANT_LOCAL_BASE_DIR_DESC: 起始目錄-必需
MANAGE_CONSTANT_LOCAL_BASE_DIR_PLACEHOLDER: 請填寫起始目錄
MANAGE_CONSTANT_LOCAL_CUSTOM_URL_DESC: 自定義網域-可選
MANAGE_CONSTANT_LOCAL_CUSTOM_URL_PLACEHOLDER: '例如:https://example.com'
MANAGE_CONSTANT_LOCAL_CUSTOM_URL_TOOLTIP: 如果您的目錄支援自定義網域,請填寫
MANAGE_CONSTANT_LOCAL_WEB_PATH: 網路路徑-可選
MANAGE_CONSTANT_LOCAL_WEB_PATH_PLACEHOLDER: '例如:test/ttc'
MANAGE_CONSTANT_LOCAL_WEB_PATH_TOOLTIP: '用於拼接網址'
MANAGE_CONSTANT_LOCAL_EXPLAIN: '本地配置'
MANAGE_CONSTANT_LOCAL_REFER_TEXT: '配置教程請參考: '
MANAGE_CONSTANT_LOCAL_BASE_DIR_RULE_MESSAGE: 起始目錄不能為空
MANAGE_CONSTANT_LOCAL_BUCKET_DESC: 特殊配置
MANAGE_CONSTANT_LOCAL_BUCKET_PLACEHOLDER: '例如:bucket1'
MANAGE_CONSTANT_LOCAL_BUCKET_TOOLTIP: 此處不可修改,僅為軟體相容性考量
MANAGE_CONSTANT_SFTP_NAME: SFTP
MANAGE_CONSTANT_SFTP_ALIAS_DESC: 配置別名-必需
MANAGE_CONSTANT_SFTP_ALIAS_PLACEHOLDER: 該配置的唯一標識
MANAGE_CONSTANT_SFTP_HOST_DESC: 地址-必需
MANAGE_CONSTANT_SFTP_HOST_PLACEHOLDER: '例如:233.233.233.233'
MANAGE_CONSTANT_SFTP_PORT_DESC: 端口-必需
MANAGE_CONSTANT_SFTP_PORT_PLACEHOLDER: '例如:22'
MANAGE_CONSTANT_SFTP_USERNAME_DESC: 用戶名
MANAGE_CONSTANT_SFTP_USERNAME_PLACEHOLDER: 請填寫用戶名
MANAGE_CONSTANT_SFTP_PASSWORD_DESC: 密碼
MANAGE_CONSTANT_SFTP_PASSWORD_PLACEHOLDER: 請填寫密碼
MANAGE_CONSTANT_SFTP_PRIVATE_KEY_DESC: 私鑰地址
MANAGE_CONSTANT_SFTP_PRIVATE_KEY_PLACEHOLDER: 請填寫私鑰地址
MANAGE_CONSTANT_SFTP_PRIVATE_KEY_TOOLTIP: '例如:/home/user/.ssh/id_rsa'
MANAGE_CONSTANT_SFTP_PASSPHRASE_DESC: 私鑰密碼
MANAGE_CONSTANT_SFTP_PASSPHRASE_PLACEHOLDER: 請填寫私鑰密碼
MANAGE_CONSTANT_SFTP_BASE_DIR_DESC: 起始目錄-可選
MANAGE_CONSTANT_SFTP_BASE_DIR_PLACEHOLDER: '例如:/dir1'
MANAGE_CONSTANT_SFTP_CUSTOM_URL_DESC: 自定義網域-可選
MANAGE_CONSTANT_SFTP_CUSTOM_URL_PLACEHOLDER: '例如:https://example.com'
MANAGE_CONSTANT_SFTP_CUSTOM_URL_TOOLTIP: 如果您的目錄支援自定義網域,請填寫
MANAGE_CONSTANT_SFTP_WEB_PATH: 網路路徑-可選
MANAGE_CONSTANT_SFTP_WEB_PATH_PLACEHOLDER: '例如:test/ttc'
MANAGE_CONSTANT_SFTP_WEB_PATH_TOOLTIP: '用於拼接網址'
MANAGE_CONSTANT_SFTP_FILE_PERMISSIONS_DESC: 文件權限
MANAGE_CONSTANT_SFTP_FILE_PERMISSIONS_PLACEHOLDER: '例如:0644'
MANAGE_CONSTANT_SFTP_FILE_PERMISSIONS_TOOLTIP: '用於設置上傳文件的權限'
MANAGE_CONSTANT_SFTP_DIR_PERMISSIONS_DESC: 目錄權限
MANAGE_CONSTANT_SFTP_DIR_PERMISSIONS_PLACEHOLDER: '例如:0755'
MANAGE_CONSTANT_SFTP_DIR_PERMISSIONS_TOOLTIP: '用於設置上傳目錄的權限'
MANAGE_CONSTANT_SFTP_EXPLAIN: 'SFTP配置'
MANAGE_CONSTANT_SFTP_REFER_TEXT: '配置教程請參考: '
MANAGE_CONSTANT_SFTP_BASE_DIR_RULE_MESSAGE: 起始目錄不能為空
MANAGE_CONSTANT_SFTP_BUCKET_DESC: 特殊配置
MANAGE_CONSTANT_SFTP_BUCKET_PLACEHOLDER: '例如:bucket1'
MANAGE_CONSTANT_SFTP_BUCKET_TOOLTIP: 此處不可修改,僅為軟體相容性考量
MANAGE_LOGIN_PAGE_PANE_NAME: 已保存配置 MANAGE_LOGIN_PAGE_PANE_NAME: 已保存配置
MANAGE_LOGIN_PAGE_PANE_DESC: 點擊圖標和別名可查看詳情,點擊進入可查看檔案頁面,點擊刪除可刪除配置 MANAGE_LOGIN_PAGE_PANE_DESC: 點擊圖標和別名可查看詳情,點擊進入可查看檔案頁面,點擊刪除可刪除配置
MANAGE_LOGIN_PAGE_PANE_LOADING: 導入配置... MANAGE_LOGIN_PAGE_PANE_LOADING: 導入配置...
@@ -782,6 +844,7 @@ MANAGE_BUCKET_DELETE_SUCCESS: 刪除成功
MANAGE_BUCKET_DELETE_FAIL: 刪除失敗 MANAGE_BUCKET_DELETE_FAIL: 刪除失敗
MANAGE_BUCKET_DELETE_CANCEL: 已取消删除 MANAGE_BUCKET_DELETE_CANCEL: 已取消删除
MANAGE_BUCKET_RENAME_INFO_MSG: 新文件名和原文件名相同,無需重命名 MANAGE_BUCKET_RENAME_INFO_MSG: 新文件名和原文件名相同,無需重命名
MANAGE_BUCKET_RENAME_SUCCESS: 重命名成功
MANAGE_BUCKET_RENAME_ERROR_MSG: 重命名失敗 MANAGE_BUCKET_RENAME_ERROR_MSG: 重命名失敗
MANAGE_BUCKET_DOWNLOAD_COLUMN_FILENAME: 文件名 MANAGE_BUCKET_DOWNLOAD_COLUMN_FILENAME: 文件名
MANAGE_BUCKET_DOWNLOAD_COLUMN_FINISHTIME: 完成時間 MANAGE_BUCKET_DOWNLOAD_COLUMN_FINISHTIME: 完成時間
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 50 KiB

+2 -2
View File
@@ -29,8 +29,8 @@ const uploadFile = async () => {
secretAccessKey: SECRET_KEY secretAccessKey: SECRET_KEY
}, },
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`, endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
sslEnabled: true, tls: true,
region: 'us-east-1' region: 'auto'
} }
const client = new S3Client.S3Client(options) const client = new S3Client.S3Client(options)
const parallelUploads3 = new Upload.Upload({ const parallelUploads3 = new Upload.Upload({
+80 -63
View File
@@ -1,6 +1,6 @@
// upload dist bundled-app to r2 // upload dist bundled-app to r2
require('dotenv').config() require('dotenv').config()
const S3 = require('aws-sdk/clients/s3')
const S3Client = require('@aws-sdk/client-s3') const S3Client = require('@aws-sdk/client-s3')
const Upload = require('@aws-sdk/lib-storage') const Upload = require('@aws-sdk/lib-storage')
const pkg = require('../package.json') const pkg = require('../package.json')
@@ -8,6 +8,7 @@ const configList = require('./config')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const yaml = require('js-yaml') const yaml = require('js-yaml')
const mime = require('mime-types')
const BUCKET = 'piclist-dl' const BUCKET = 'piclist-dl'
const VERSION = pkg.version const VERSION = pkg.version
@@ -16,12 +17,15 @@ const ACCOUNT_ID = process.env.R2_ACCOUNT_ID
const SECRET_ID = process.env.R2_SECRET_ID const SECRET_ID = process.env.R2_SECRET_ID
const SECRET_KEY = process.env.R2_SECRET_KEY const SECRET_KEY = process.env.R2_SECRET_KEY
const s3 = new S3({ const options = {
credentials: {
accessKeyId: SECRET_ID,
secretAccessKey: SECRET_KEY
},
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`, endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
accessKeyId: SECRET_ID, tls: true,
secretAccessKey: SECRET_KEY, region: 'auto'
signatureVersion: 'v4' }
})
const removeDupField = path => { const removeDupField = path => {
const file = fs.readFileSync(path, 'utf8') const file = fs.readFileSync(path, 'utf8')
@@ -39,74 +43,87 @@ const removeDupField = path => {
const uploadFile = async () => { const uploadFile = async () => {
try { try {
const platform = process.platform const platform = process.platform
if (configList[platform]) { if (!configList[platform]) {
let versionFileHasUploaded = false console.warn('platform not supported!', platform)
for (const [index, config] of configList[platform].entries()) { return
const fileName = `${config.appNameWithPrefix}${VERSION}${config.arch}${config.ext}` }
const distPath = path.join(__dirname, '../dist_electron') let versionFileHasUploaded = false
const versionFileName = config['version-file'] for (const [index, config] of configList[platform].entries()) {
console.log('[PicList Dist] Uploading...', fileName, `${index + 1}/${configList[platform].length}`) const fileName = `${config.appNameWithPrefix}${VERSION}${config.arch}${config.ext}`
const fileStream = fs.createReadStream(path.join(distPath, fileName)) const distPath = path.join(__dirname, '../dist_electron')
const options = { const versionFileName = config['version-file']
credentials: {
accessKeyId: SECRET_ID, console.log('[PicList Dist] Uploading...', fileName, `${index + 1}/${configList[platform].length}`)
secretAccessKey: SECRET_KEY const fileStream = fs.createReadStream(path.join(distPath, fileName))
}, const client = new S3Client.S3Client(options)
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
sslEnabled: true, const parallelUploads3 = new Upload.Upload({
region: 'us-east-1' client,
params: {
Bucket: BUCKET,
Key: `${FILE_PATH}${fileName}`,
Body: fileStream,
ContentType: 'application/octet-stream',
Metadata: {
description: 'uploaded by PicList'
}
} }
const client = new S3Client.S3Client(options) })
const parallelUploads3 = new Upload.Upload({ parallelUploads3.on('httpUploadProgress', progress => {
const progressBar = Math.round((progress.loaded / progress.total) * 100)
process.stdout.write(`\r${progressBar}% ${fileName}`)
})
console.log('\n')
await parallelUploads3.done()
console.log(`${fileName} uploaded!`)
if (!versionFileHasUploaded) {
console.log('[PicList Version File] Uploading...', versionFileName)
let versionFilePath
if (platform === 'win32') {
versionFilePath = path.join(distPath, 'latest.yml')
} else if (platform === 'darwin') {
versionFilePath = path.join(distPath, 'latest-mac.yml')
} else {
versionFilePath = path.join(distPath, 'latest-linux.yml')
}
removeDupField(versionFilePath)
const versionFileStream = fs.createReadStream(versionFilePath)
const uploadVersionFileToRoot = new Upload.Upload({
client, client,
params: { params: {
Bucket: BUCKET, Bucket: BUCKET,
Key: `${FILE_PATH}${fileName}`, Key: `${versionFileName}`,
Body: fileStream, Body: versionFileStream,
ContentType: 'application/octet-stream', ContentType: mime.lookup(versionFileName),
Metadata: { Metadata: {
description: 'uploaded by PicList' description: 'uploaded by PicList'
} }
} }
}) })
parallelUploads3.on('httpUploadProgress', progress => { console.log('\nUploading version file to root...')
const progressBar = Math.round((progress.loaded / progress.total) * 100) await uploadVersionFileToRoot.done()
process.stdout.write(`\r${progressBar}% ${fileName}`) console.log(`${versionFileName} uploaded!`)
}) versionFileStream.close()
console.log('\n') const versionFileStream2 = fs.createReadStream(versionFilePath)
await parallelUploads3.done() const uploadVersionFileToLatest = new Upload.Upload({
console.log(`${fileName} uploaded!`) client,
if (!versionFileHasUploaded) { params: {
console.log('[PicList Version File] Uploading...', versionFileName) Bucket: BUCKET,
let versionFilePath Key: `${FILE_PATH}${versionFileName}`,
if (platform === 'win32') { Body: versionFileStream2,
versionFilePath = path.join(distPath, 'latest.yml') ContentType: mime.lookup(versionFileName),
} else if (platform === 'darwin') { Metadata: {
versionFilePath = path.join(distPath, 'latest-mac.yml') description: 'uploaded by PicList'
} else { }
versionFilePath = path.join(distPath, 'latest-linux.yml')
} }
removeDupField(versionFilePath) })
const versionFileBuffer = fs.readFileSync(versionFilePath) console.log('\nUploading version file to latest...')
await s3 await uploadVersionFileToLatest.done()
.upload({ console.log(`${versionFileName} uploaded!`)
Bucket: BUCKET, versionFileStream2.close()
Key: `${versionFileName}`, versionFileHasUploaded = true
Body: versionFileBuffer
})
.promise()
await s3
.upload({
Bucket: BUCKET,
Key: `${FILE_PATH}${versionFileName}`,
Body: versionFileBuffer
})
.promise()
versionFileHasUploaded = true
}
} }
} else {
console.warn('platform not supported!', platform)
} }
} catch (err) { } catch (err) {
console.error(err) console.error(err)
+30 -10
View File
@@ -1,25 +1,43 @@
// Vue 相关
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './renderer/App.vue' import App from './renderer/App.vue'
import router from './renderer/router' import router from './renderer/router'
import ElementUI from 'element-plus' import ElementUI from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import { webFrame } from 'electron'
import VueLazyLoad from 'vue3-lazyload' import VueLazyLoad from 'vue3-lazyload'
import axios from 'axios'
import { mainMixin } from './renderer/utils/mainMixin'
import { dragMixin } from '@/utils/mixin'
import db from './renderer/utils/db'
import { i18nManager, T } from './renderer/i18n/index'
import { getConfig, saveConfig, sendToMain, triggerRPC } from '@/utils/dataSender'
import { store } from '@/store'
import vue3PhotoPreview from 'vue3-photo-preview' import vue3PhotoPreview from 'vue3-photo-preview'
import 'vue3-photo-preview/dist/index.css' import 'vue3-photo-preview/dist/index.css'
import VueVideoPlayer from '@videojs-player/vue'
// Electron 相关
import { webFrame } from 'electron'
// Axios
import axios from 'axios'
// Mixins
import { mainMixin } from './renderer/utils/mainMixin'
import { dragMixin } from '@/utils/mixin'
// 数据库
import db from './renderer/utils/db'
// 国际化
import { i18nManager, T } from './renderer/i18n/index'
// 工具函数
import { getConfig, saveConfig, sendToMain, triggerRPC } from '@/utils/dataSender'
// 状态管理
import { store } from '@/store'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// 代码高亮
import 'highlight.js/styles/atom-one-dark.css' import 'highlight.js/styles/atom-one-dark.css'
import hljsVuePlugin from '@highlightjs/vue-plugin' import hljsVuePlugin from '@highlightjs/vue-plugin'
import hljsCommon from 'highlight.js/lib/common' import hljsCommon from 'highlight.js/lib/common'
import VueVideoPlayer from '@videojs-player/vue'
import 'video.js/dist/video-js.css' import 'video.js/dist/video-js.css'
webFrame.setVisualZoomLevelLimits(1, 1) webFrame.setVisualZoomLevelLimits(1, 1)
@@ -34,7 +52,9 @@ app.config.globalProperties.$builtInPicBed = [
'upyun', 'upyun',
'aliyun', 'aliyun',
'github', 'github',
'webdavplist' 'webdavplist',
'local',
'sftpplist'
] ]
app.config.unwrapInjectedRef = true app.config.unwrapInjectedRef = true
+16 -7
View File
@@ -1,15 +1,25 @@
// get notice from remote // External dependencies
// such as some notices for users; some updates for users import axios from 'axios'
import fs from 'fs-extra' import fs from 'fs-extra'
import { app, clipboard, dialog, shell } from 'electron'
import { IRemoteNoticeActionType, IRemoteNoticeTriggerCount, IRemoteNoticeTriggerHook } from '#/types/enum'
import { lte, gte } from 'semver'
import path from 'path' import path from 'path'
import axios from 'axios' // Electron modules
import { app, clipboard, dialog, shell } from 'electron'
// Custom modules and utilities
import windowManager from '../window/windowManager' import windowManager from '../window/windowManager'
import { showNotification } from '~/main/utils/common' import { showNotification } from '~/main/utils/common'
// Custom types/enums
import {
IRemoteNoticeActionType,
IRemoteNoticeTriggerCount,
IRemoteNoticeTriggerHook
} from '#/types/enum'
// External utility functions
import { gte, lte } from 'semver'
// for test // for test
const REMOTE_NOTICE_URL = 'https://release.piclist.cn/remote-notice.json' const REMOTE_NOTICE_URL = 'https://release.piclist.cn/remote-notice.json'
@@ -37,7 +47,6 @@ class RemoteNoticeHandler {
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 this.remoteNoticeLocalCountStorage = localCountStorage
} catch (e) { } catch (e) {
console.log(e)
this.remoteNoticeLocalCountStorage = localCountStorage this.remoteNoticeLocalCountStorage = localCountStorage
} }
} }
+15 -5
View File
@@ -1,14 +1,24 @@
import bus from '@core/bus' // External dependencies
import { import {
globalShortcut globalShortcut
} from 'electron' } from 'electron'
import logger from '@core/picgo/logger'
import GuiApi from '../../gui' import bus from '@core/bus'
import db from '~/main/apis/core/datastore' import db from '~/main/apis/core/datastore'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants' import logger from '@core/picgo/logger'
import shortKeyService from './shortKeyService'
import picgo from '@core/picgo' import picgo from '@core/picgo'
// Electron modules
// Custom utilities and modules
import GuiApi from '../../gui'
import shortKeyService from './shortKeyService'
// Custom types/enums
// External utility functions
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
class ShortKeyHandler { class ShortKeyHandler {
private isInModifiedMode: boolean = false private isInModifiedMode: boolean = false
constructor () { constructor () {
@@ -1,4 +1,5 @@
import logger from '@core/picgo/logger' import logger from '@core/picgo/logger'
class ShortKeyService { class ShortKeyService {
private commandList: Map<string, IShortKeyHandler> = new Map() private commandList: Map<string, IShortKeyHandler> = new Map()
registerCommand (command: string, handler: IShortKeyHandler) { registerCommand (command: string, handler: IShortKeyHandler) {
+7 -1
View File
@@ -1,4 +1,8 @@
// External dependencies
import fs from 'fs-extra' import fs from 'fs-extra'
import { cloneDeep } from 'lodash'
// Electron modules
import { import {
app, app,
Menu, Menu,
@@ -9,6 +13,8 @@ import {
screen, screen,
nativeTheme nativeTheme
} from 'electron' } from 'electron'
// Custom utilities and modules
import uploader from 'apis/app/uploader' import uploader from 'apis/app/uploader'
import db, { GalleryDB } from '~/main/apis/core/datastore' import db, { GalleryDB } from '~/main/apis/core/datastore'
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
@@ -22,7 +28,7 @@ import { buildPicBedListMenu } from '~/main/events/remotes/menu'
import clipboardPoll from '~/main/utils/clipboardPoll' import clipboardPoll from '~/main/utils/clipboardPoll'
import picgo from '../../core/picgo' import picgo from '../../core/picgo'
import { uploadClipboardFiles } from '../uploader/apis' import { uploadClipboardFiles } from '../uploader/apis'
import { cloneDeep } from 'lodash'
let contextMenu: Menu | null let contextMenu: Menu | null
let tray: Tray | null let tray: Tray | null
+45 -21
View File
@@ -1,10 +1,15 @@
// External dependencies
import fs from 'fs-extra'
import { cloneDeep } from 'lodash'
// Electron modules
import { import {
Notification, Notification,
WebContents WebContents
} from 'electron' } from 'electron'
// Custom utilities and modules
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import uploader from '.'
import pasteTemplate from '~/main/utils/pasteTemplate' import pasteTemplate from '~/main/utils/pasteTemplate'
import db, { GalleryDB } from '~/main/apis/core/datastore' import db, { GalleryDB } from '~/main/apis/core/datastore'
import { handleCopyUrl, handleUrlEncodeWithSetting } from '~/main/utils/common' import { handleCopyUrl, handleUrlEncodeWithSetting } from '~/main/utils/common'
@@ -12,8 +17,13 @@ import { T } from '~/main/i18n/index'
import ALLApi from '@/apis/allApi' import ALLApi from '@/apis/allApi'
import picgo from '@core/picgo' import picgo from '@core/picgo'
import GuiApi from '../../gui' import GuiApi from '../../gui'
import fs from 'fs-extra' import uploader from '.'
import { cloneDeep } from 'lodash' import { IWindowList } from '#/types/enum'
import { picBedsCanbeDeleted } from '#/utils/static'
import path from 'path'
import SSHClient from '~/main/utils/sshClient'
import { ISftpPlistConfig } from 'piclist'
import { getRawData } from '~/renderer/utils/common'
const handleClipboardUploading = async (): Promise<false | ImgInfo[]> => { const handleClipboardUploading = async (): Promise<false | ImgInfo[]> => {
const useBuiltinClipboard = db.get('settings.useBuiltinClipboard') === undefined ? true : !!db.get('settings.useBuiltinClipboard') const useBuiltinClipboard = db.get('settings.useBuiltinClipboard') === undefined ? true : !!db.get('settings.useBuiltinClipboard')
@@ -120,9 +130,23 @@ export const uploadChoosedFiles = async (webContents: WebContents, files: IFileW
} }
} }
async function deleteWebdavFile (config: ISftpPlistConfig, fileName: string) {
try {
const client = SSHClient.instance
await client.connect(config)
const uploadPath = `/${(config.uploadPath || '')}/`.replace(/\/+/g, '/')
const remote = path.join(uploadPath, fileName)
const deleteResult = await client.deleteFile(remote)
client.close()
return deleteResult
} catch (err: any) {
console.error(err)
return false
}
}
export const deleteChoosedFiles = async (list: ImgInfo[]): Promise<boolean[]> => { export const deleteChoosedFiles = async (list: ImgInfo[]): Promise<boolean[]> => {
const result = [] const result = []
const picBedsCanbeDeleted = ['smms', 'github', 'imgur', 'tcyun', 'aliyun', 'qiniu', 'upyun', 'aws-s3', 'webdavplist']
for (const item of list) { for (const item of list) {
if (item.id) { if (item.id) {
try { try {
@@ -130,23 +154,23 @@ export const deleteChoosedFiles = async (list: ImgInfo[]): Promise<boolean[]> =>
const file = await dbStore.removeById(item.id) const file = await dbStore.removeById(item.id)
if (await picgo.getConfig('settings.deleteCloudFile')) { if (await picgo.getConfig('settings.deleteCloudFile')) {
if (item.type !== undefined && picBedsCanbeDeleted.includes(item.type)) { if (item.type !== undefined && picBedsCanbeDeleted.includes(item.type)) {
setTimeout(() => { const noteFunc = (value: boolean) => {
ALLApi.delete(item).then((value: boolean) => { const notification = new Notification({
if (value) { title: T('MANAGE_BUCKET_BATCH_DELETE_ERROR_MSG_MSG2'),
const notification = new Notification({ body: T(value ? 'GALLERY_SYNC_DELETE_NOTICE_SUCCEED' : 'GALLERY_SYNC_DELETE_NOTICE_FAILED')
title: T('MANAGE_BUCKET_BATCH_DELETE_ERROR_MSG_MSG2'),
body: T('GALLERY_SYNC_DELETE_NOTICE_SUCCEED')
})
notification.show()
} else {
const notification = new Notification({
title: T('MANAGE_BUCKET_BATCH_DELETE_ERROR_MSG_MSG2'),
body: T('GALLERY_SYNC_DELETE_NOTICE_FAILED')
})
notification.show()
}
}) })
}, 0) notification.show()
}
if (item.type === 'webdavplist') {
const { fileName, config } = item
setTimeout(() => {
deleteWebdavFile(getRawData(config), fileName || '').then(noteFunc)
}, 0)
} else {
setTimeout(() => {
ALLApi.delete(item).then(noteFunc)
}, 0)
}
} }
} }
setTimeout(() => { setTimeout(() => {
+20 -8
View File
@@ -1,3 +1,11 @@
// External dependencies
import dayjs from 'dayjs'
import util from 'util'
import path from 'path'
import writeFile from 'write-file-atomic'
import fse from 'fs-extra'
// Electron modules
import { import {
Notification, Notification,
BrowserWindow, BrowserWindow,
@@ -5,22 +13,26 @@ import {
WebContents, WebContents,
clipboard clipboard
} from 'electron' } from 'electron'
import dayjs from 'dayjs'
// Custom utilities and modules
import picgo from '@core/picgo' import picgo from '@core/picgo'
import db from '~/main/apis/core/datastore' import db from '~/main/apis/core/datastore'
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import util from 'util'
import { IPicGo } from 'piclist'
import { showNotification, getClipboardFilePath } from '~/main/utils/common' import { showNotification, getClipboardFilePath } from '~/main/utils/common'
import { GET_RENAME_FILE_NAME, RENAME_FILE_NAME } from '~/universal/events/constants'
import logger from '@core/picgo/logger' import logger from '@core/picgo/logger'
import { T } from '~/main/i18n' import { T } from '~/main/i18n'
import fse from 'fs-extra'
import path from 'path'
import writeFile from 'write-file-atomic'
import { CLIPBOARD_IMAGE_FOLDER } from '~/universal/utils/static' import { CLIPBOARD_IMAGE_FOLDER } from '~/universal/utils/static'
// Custom types/enums
import { IWindowList } from '#/types/enum'
// External utility functions
import { IPicGo } from 'piclist'
import {
GET_RENAME_FILE_NAME,
RENAME_FILE_NAME
} from '~/universal/events/constants'
const waitForRename = (window: BrowserWindow, id: number): Promise<string|null> => { const waitForRename = (window: BrowserWindow, id: number): Promise<string|null> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const windowId = window.id const windowId = window.id
+15 -6
View File
@@ -1,3 +1,14 @@
// External dependencies
import { app } from 'electron'
// Electron modules
// Custom utilities and modules
import bus from '@core/bus'
import db from '~/main/apis/core/datastore'
import picgo from '~/main/apis/core/picgo'
import { T } from '~/main/i18n'
import { remoteNoticeHandler } from '../remoteNotice'
import { import {
SETTING_WINDOW_URL, SETTING_WINDOW_URL,
TRAY_WINDOW_URL, TRAY_WINDOW_URL,
@@ -5,15 +16,13 @@ import {
RENAME_WINDOW_URL, RENAME_WINDOW_URL,
TOOLBOX_WINDOW_URL TOOLBOX_WINDOW_URL
} from './constants' } from './constants'
// Custom types/enums
import { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum' import { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum'
import bus from '@core/bus'
// External utility functions
import { CREATE_APP_MENU } from '@core/bus/constants' import { CREATE_APP_MENU } from '@core/bus/constants'
import db from '~/main/apis/core/datastore'
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants' import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import { app } from 'electron'
import { remoteNoticeHandler } from '../remoteNotice'
import picgo from '~/main/apis/core/picgo'
import { T } from '~/main/i18n'
const windowList = new Map<IWindowList, IWindowListItem>() const windowList = new Map<IWindowList, IWindowListItem>()
+8 -3
View File
@@ -1,7 +1,12 @@
import { // External dependencies
BrowserWindow
} from 'electron'
import windowList from './windowList' import windowList from './windowList'
// Electron modules
import { BrowserWindow } from 'electron'
// Custom utilities and modules
// Custom types/enums
import { IWindowList } from '#/types/enum' import { IWindowList } from '#/types/enum'
class WindowManager implements IWindowManager { class WindowManager implements IWindowManager {
+18 -4
View File
@@ -1,10 +1,24 @@
// External dependencies
import fs from 'fs-extra' import fs from 'fs-extra'
import writeFile from 'write-file-atomic'
import path from 'path'
import { app } from 'electron'
import { getLogger } from '../utils/localLogger'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import path from 'path'
// Electron modules
import { app } from 'electron'
// Custom utilities and modules
import { getLogger } from '../utils/localLogger'
// Custom types/enums
// External utility functions
// External utility functions
import writeFile from 'write-file-atomic'
// Custom types/enums
import { T } from '~/main/i18n' import { T } from '~/main/i18n'
const STORE_PATH = app.getPath('userData') const STORE_PATH = app.getPath('userData')
const configFilePath = path.join(STORE_PATH, 'data.json') const configFilePath = path.join(STORE_PATH, 'data.json')
const configFileBackupPath = path.join(STORE_PATH, 'data.bak.json') const configFileBackupPath = path.join(STORE_PATH, 'data.bak.json')
+11
View File
@@ -1,6 +1,17 @@
// External dependencies
import fs from 'fs-extra' import fs from 'fs-extra'
// Electron modules
// Custom utilities and modules
import { dbPathChecker, dbPathDir, getGalleryDBPath } from './dbChecker' import { dbPathChecker, dbPathDir, getGalleryDBPath } from './dbChecker'
// Custom types/enums
// External utility functions
import { DBStore, JSONStore } from '@picgo/store' import { DBStore, JSONStore } from '@picgo/store'
// External utility functions
import { T } from '~/main/i18n' import { T } from '~/main/i18n'
const STORE_PATH = dbPathDir() const STORE_PATH = dbPathDir()
+11 -2
View File
@@ -1,8 +1,17 @@
import { dbChecker, dbPathChecker } from 'apis/core/datastore/dbChecker' // External dependencies
import pkg from 'root/package.json' import pkg from 'root/package.json'
import debounce from 'lodash/debounce'
// Electron modules
// Custom utilities and modules
import { PicGo } from 'piclist' import { PicGo } from 'piclist'
import db from 'apis/core/datastore' import db from 'apis/core/datastore'
import debounce from 'lodash/debounce' import { dbChecker, dbPathChecker } from 'apis/core/datastore/dbChecker'
// Custom types/enums
// External utility functions
const CONFIG_PATH = dbPathChecker() const CONFIG_PATH = dbPathChecker()
+13 -9
View File
@@ -1,25 +1,29 @@
// External dependencies
import fs from 'fs-extra'
import { cloneDeep } from 'lodash'
// Electron modules
import { import {
dialog, dialog,
BrowserWindow, BrowserWindow,
Notification, Notification,
ipcMain ipcMain
} from 'electron' } from 'electron'
// Custom utilities and modules
import db, { GalleryDB } from 'apis/core/datastore' import db, { GalleryDB } from 'apis/core/datastore'
import { dbPathChecker, defaultConfigPath, getGalleryDBPath } from 'apis/core/datastore/dbChecker' import { dbPathChecker, defaultConfigPath, getGalleryDBPath } from 'apis/core/datastore/dbChecker'
import uploader from 'apis/app/uploader' import uploader from 'apis/app/uploader'
import pasteTemplate from '~/main/utils/pasteTemplate' import pasteTemplate from '~/main/utils/pasteTemplate'
import { handleCopyUrl } from '~/main/utils/common' import { handleCopyUrl } from '~/main/utils/common'
import { import { getWindowId, getSettingWindowId } from '@core/bus/apis'
getWindowId, import { SHOW_INPUT_BOX } from '~/universal/events/constants'
getSettingWindowId
} from '@core/bus/apis' // Custom types/enums
import {
SHOW_INPUT_BOX // External utility functions
} from '~/universal/events/constants'
import { DBStore } from '@picgo/store' import { DBStore } from '@picgo/store'
import { T } from '~/main/i18n' import { T } from '~/main/i18n'
import fs from 'fs-extra'
import { cloneDeep } from 'lodash'
// Cross-process support may be required in the future // Cross-process support may be required in the future
class GuiApi implements IGuiApi { class GuiApi implements IGuiApi {
+11 -1
View File
@@ -1,4 +1,9 @@
// External dependencies
import bus from '@core/bus' import bus from '@core/bus'
// Electron modules
// Custom utilities and modules
import { import {
uploadClipboardFiles, uploadClipboardFiles,
uploadChoosedFiles uploadChoosedFiles
@@ -6,8 +11,12 @@ import {
import { import {
createMenu createMenu
} from 'apis/app/system' } from 'apis/app/system'
import { IWindowList } from '#/types/enum'
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
// Custom types/enums
import { IWindowList } from '#/types/enum'
// External utility functions
import { import {
UPLOAD_WITH_FILES, UPLOAD_WITH_FILES,
UPLOAD_WITH_FILES_RESPONSE, UPLOAD_WITH_FILES_RESPONSE,
@@ -19,6 +28,7 @@ import {
GET_SETTING_WINDOW_ID_RESPONSE, GET_SETTING_WINDOW_ID_RESPONSE,
CREATE_APP_MENU CREATE_APP_MENU
} from '@core/bus/constants' } from '@core/bus/constants'
function initEventCenter () { function initEventCenter () {
const eventList: any = { const eventList: any = {
'picgo:upload': uploadClipboardFiles, 'picgo:upload': uploadClipboardFiles,
+84
View File
@@ -1,3 +1,4 @@
// Electron 相关
import { import {
app, app,
ipcMain, ipcMain,
@@ -8,16 +9,38 @@ import {
screen, screen,
IpcMainInvokeEvent IpcMainInvokeEvent
} from 'electron' } from 'electron'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum' import { IWindowList } from '#/types/enum'
// 上传器
import uploader from 'apis/app/uploader' import uploader from 'apis/app/uploader'
// 粘贴模板函数
import pasteTemplate from '~/main/utils/pasteTemplate' import pasteTemplate from '~/main/utils/pasteTemplate'
// 数据存储库和类型声明
import db, { GalleryDB } from '~/main/apis/core/datastore' import db, { GalleryDB } from '~/main/apis/core/datastore'
// 服务器模块
import server from '~/main/server' import server from '~/main/server'
// 获取图片床模块
import getPicBeds from '~/main/utils/getPicBeds' import getPicBeds from '~/main/utils/getPicBeds'
// 快捷键处理器
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler' import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
// 全局事件总线
import bus from '@core/bus' import bus from '@core/bus'
// 文件系统库
import fs from 'fs-extra' import fs from 'fs-extra'
// 事件常量
import { import {
TOGGLE_SHORTKEY_MODIFIED_MODE, TOGGLE_SHORTKEY_MODIFIED_MODE,
OPEN_DEVTOOLS, OPEN_DEVTOOLS,
@@ -34,22 +57,45 @@ import {
GET_PICBEDS, GET_PICBEDS,
HIDE_DOCK HIDE_DOCK
} from '#/events/constants' } from '#/events/constants'
// 上传剪贴板文件和已选文件的函数
import { import {
uploadClipboardFiles, uploadClipboardFiles,
uploadChoosedFiles uploadChoosedFiles
} from '~/main/apis/app/uploader/apis' } from '~/main/apis/app/uploader/apis'
// 核心 IPC 模块
import picgoCoreIPC from './picgoCoreIPC' import picgoCoreIPC from './picgoCoreIPC'
// 处理复制的 URL 和生成短链接的函数
import { handleCopyUrl, generateShortUrl } from '~/main/utils/common' import { handleCopyUrl, generateShortUrl } from '~/main/utils/common'
// 构建主页面、迷你页面、插件页面、图片床列表的菜单函数
import { buildMainPageMenu, buildMiniPageMenu, buildPluginPageMenu, buildPicBedListMenu } from './remotes/menu' import { buildMainPageMenu, buildMiniPageMenu, buildPluginPageMenu, buildPicBedListMenu } from './remotes/menu'
// 路径处理库
import path from 'path' import path from 'path'
// i18n 模块
import { T } from '~/main/i18n' import { T } from '~/main/i18n'
// 同步设置的上传和下载文件函数
import { uploadFile, downloadFile } from '../utils/syncSettings' import { uploadFile, downloadFile } from '../utils/syncSettings'
// SSH 客户端模块
import SSHClient from '../utils/sshClient'
// Sftp 配置类型声明
import { ISftpPlistConfig } from 'piclist'
import { removeFileFromS3InMain, removeFileFromDogeInMain, removeFileFromHuaweiInMain } from '~/main/utils/deleteFunc'
const STORE_PATH = app.getPath('userData') const STORE_PATH = app.getPath('userData')
export default { export default {
listen () { listen () {
picgoCoreIPC.listen() picgoCoreIPC.listen()
// Upload Related IPC
// from macOS tray // from macOS tray
ipcMain.on('uploadClipboardFiles', async () => { ipcMain.on('uploadClipboardFiles', async () => {
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)! const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
@@ -85,6 +131,7 @@ export default {
return uploadChoosedFiles(evt.sender, files) return uploadChoosedFiles(evt.sender, files)
}) })
// ShortKey Related IPC
ipcMain.on('updateShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, oldKey: string, from: string) => { ipcMain.on('updateShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, oldKey: string, from: string) => {
const result = shortKeyHandler.updateShortKey(item, oldKey, from) const result = shortKeyHandler.updateShortKey(item, oldKey, from)
evt.sender.send('updateShortKeyResponse', result) evt.sender.send('updateShortKeyResponse', result)
@@ -120,6 +167,39 @@ export default {
} }
}) })
// Gallery image cloud delete IPC
ipcMain.handle('delete-sftp-file', async (_evt: IpcMainInvokeEvent, config: ISftpPlistConfig, fileName: string) => {
try {
const client = SSHClient.instance
await client.connect(config)
const uploadPath = `/${(config.uploadPath || '')}/`.replace(/\/+/g, '/')
const remote = path.join(uploadPath, fileName)
const deleteResult = await client.deleteFile(remote)
client.close()
return deleteResult
} catch (err: any) {
console.error(err)
return false
}
})
ipcMain.handle('delete-aws-s3-file', async (_evt: IpcMainInvokeEvent, configMap: IStringKeyMap) => {
const result = await removeFileFromS3InMain(configMap)
return result
})
ipcMain.handle('delete-doge-file', async (_evt: IpcMainInvokeEvent, configMap: IStringKeyMap) => {
const result = await removeFileFromDogeInMain(configMap)
return result
})
ipcMain.handle('delete-huaweicloud-file', async (_evt: IpcMainInvokeEvent, configMap: IStringKeyMap) => {
const result = await removeFileFromHuaweiInMain(configMap)
return result
})
// migrate from PicGo
ipcMain.handle('migrateFromPicGo', async () => { ipcMain.handle('migrateFromPicGo', async () => {
const picGoConfigPath = STORE_PATH.replace('piclist', 'picgo') const picGoConfigPath = STORE_PATH.replace('piclist', 'picgo')
const fileToMigration = [ const fileToMigration = [
@@ -147,6 +227,8 @@ export default {
} }
}) })
// PicList Setting page IPC
ipcMain.on('updateCustomLink', () => { ipcMain.on('updateCustomLink', () => {
const notification = new Notification({ const notification = new Notification({
title: T('OPERATION_SUCCEED'), title: T('OPERATION_SUCCEED'),
@@ -212,6 +294,8 @@ export default {
mainWindow.setAlwaysOnTop(!isAlwaysOnTop) mainWindow.setAlwaysOnTop(!isAlwaysOnTop)
}) })
// Window operation API
ipcMain.on('openSettingWindow', () => { ipcMain.on('openSettingWindow', () => {
windowManager.get(IWindowList.SETTING_WINDOW)!.show() windowManager.get(IWindowList.SETTING_WINDOW)!.show()
const autoCloseMiniWindow = db.get('settings.autoCloseMiniWindow') || false const autoCloseMiniWindow = db.get('settings.autoCloseMiniWindow') || false
+25 -10
View File
@@ -1,5 +1,5 @@
// External dependencies
import path from 'path' import path from 'path'
import GuiApi from 'apis/gui'
import { import {
dialog, dialog,
shell, shell,
@@ -7,7 +7,11 @@ import {
ipcMain, ipcMain,
clipboard clipboard
} from 'electron' } from 'electron'
import { IPasteStyle, IPicGoHelperType, IWindowList } from '#/types/enum'
// Electron modules
// Custom utilities and modules
import GuiApi from 'apis/gui'
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler' import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
import picgo from '@core/picgo' import picgo from '@core/picgo'
import { handleStreamlinePluginName, simpleClone } from '~/universal/utils/common' import { handleStreamlinePluginName, simpleClone } from '~/universal/utils/common'
@@ -15,6 +19,16 @@ import { IGuiMenuItem, PicGo as PicGoCore } from 'piclist'
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
import { showNotification } from '~/main/utils/common' import { showNotification } from '~/main/utils/common'
import { dbPathChecker } from 'apis/core/datastore/dbChecker' import { dbPathChecker } from 'apis/core/datastore/dbChecker'
import { GalleryDB } from 'apis/core/datastore'
import pasteTemplate from '../utils/pasteTemplate'
import { i18nManager, T } from '~/main/i18n'
import { rpcServer } from './rpc'
// Custom types/enums
import { IPasteStyle, IPicGoHelperType, IWindowList } from '#/types/enum'
import { IObject, IFilter } from '@picgo/store/dist/types'
// External utility functions
import { import {
PICGO_SAVE_CONFIG, PICGO_SAVE_CONFIG,
PICGO_GET_CONFIG, PICGO_GET_CONFIG,
@@ -32,12 +46,6 @@ import {
GET_CURRENT_LANGUAGE GET_CURRENT_LANGUAGE
} from '#/events/constants' } from '#/events/constants'
import { GalleryDB } from 'apis/core/datastore'
import { IObject, IFilter } from '@picgo/store/dist/types'
import pasteTemplate from '../utils/pasteTemplate'
import { i18nManager, T } from '~/main/i18n'
import { rpcServer } from './rpc'
// eslint-disable-next-line // eslint-disable-next-line
const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require
// const PluginHandler = requireFunc('picgo/lib/PluginHandler').default // const PluginHandler = requireFunc('picgo/lib/PluginHandler').default
@@ -188,10 +196,10 @@ const handlePluginUninstall = async (fullName: string) => {
dispose() dispose()
} }
const handlePluginUpdate = async (fullName: string) => { const handlePluginUpdate = async (fullName: string | string[]) => {
const window = windowManager.get(IWindowList.SETTING_WINDOW)! const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const dispose = handleNPMError() const dispose = handleNPMError()
const res = await picgo.pluginHandler.update([fullName]) const res = await picgo.pluginHandler.update(typeof fullName === 'string' ? [fullName] : fullName)
if (res.success) { if (res.success) {
window.webContents.send('updateSuccess', res.body[0]) window.webContents.send('updateSuccess', res.body[0])
} else { } else {
@@ -204,6 +212,12 @@ const handlePluginUpdate = async (fullName: string) => {
dispose() dispose()
} }
const handleUpdateAllPlugin = () => {
ipcMain.on('updateAllPlugin', async (event: IpcMainEvent, list: string[]) => {
handlePluginUpdate(list)
})
}
const handleNPMError = (): IDispose => { const handleNPMError = (): IDispose => {
const handler = (msg: string) => { const handler = (msg: string) => {
if (msg === 'NPM is not installed') { if (msg === 'NPM is not installed') {
@@ -411,6 +425,7 @@ export default {
handlePicGoGetConfig() handlePicGoGetConfig()
handlePicGoGalleryDB() handlePicGoGalleryDB()
handleImportLocalPlugin() handleImportLocalPlugin()
handleUpdateAllPlugin()
handleOpenFile() handleOpenFile()
handleOpenWindow() handleOpenWindow()
handleI18n() handleI18n()
+26 -9
View File
@@ -1,20 +1,37 @@
import windowManager from 'apis/app/window/windowManager' // External dependencies
import { IWindowList } from '#/types/enum' import pkg from 'root/package.json'
// Electron modules
import { Menu, BrowserWindow, app, dialog } from 'electron' import { Menu, BrowserWindow, app, dialog } from 'electron'
// Custom utilities and modules
import windowManager from 'apis/app/window/windowManager'
import getPicBeds from '~/main/utils/getPicBeds' import getPicBeds from '~/main/utils/getPicBeds'
import picgo from '@core/picgo' import picgo from '@core/picgo'
import {
uploadClipboardFiles
} from '~/main/apis/app/uploader/apis'
import pkg from 'root/package.json'
import GuiApi from 'apis/gui' import GuiApi from 'apis/gui'
import { PICGO_CONFIG_PLUGIN, PICGO_HANDLE_PLUGIN_DONE, PICGO_HANDLE_PLUGIN_ING, PICGO_TOGGLE_PLUGIN, SHOW_MAIN_PAGE_DONATION, SHOW_MAIN_PAGE_QRCODE } from '~/universal/events/constants'
import picgoCoreIPC from '~/main/events/picgoCoreIPC' import picgoCoreIPC from '~/main/events/picgoCoreIPC'
import { PicGo as PicGoCore } from 'piclist'
import { T } from '~/main/i18n'
import { changeCurrentUploader } from '~/main/utils/handleUploaderConfig' import { changeCurrentUploader } from '~/main/utils/handleUploaderConfig'
import db from '~/main/apis/core/datastore' import db from '~/main/apis/core/datastore'
import clipboardPoll from '~/main/utils/clipboardPoll' import clipboardPoll from '~/main/utils/clipboardPoll'
// Custom types/enums
import { IWindowList } from '#/types/enum'
// External utility functions
import {
uploadClipboardFiles
} from '~/main/apis/app/uploader/apis'
import {
PICGO_CONFIG_PLUGIN,
PICGO_HANDLE_PLUGIN_DONE,
PICGO_HANDLE_PLUGIN_ING,
PICGO_TOGGLE_PLUGIN,
SHOW_MAIN_PAGE_DONATION,
SHOW_MAIN_PAGE_QRCODE
} from '~/universal/events/constants'
import { PicGo as PicGoCore } from 'piclist'
import { T } from '~/main/i18n'
interface GuiMenuItem { interface GuiMenuItem {
label: string label: string
handle: (arg0: PicGoCore, arg1: GuiApi) => Promise<void> handle: (arg0: PicGoCore, arg1: GuiApi) => Promise<void>
+11 -2
View File
@@ -1,10 +1,19 @@
// External dependencies
import { ipcMain, IpcMainEvent } from 'electron' import { ipcMain, IpcMainEvent } from 'electron'
import { IRPCActionType } from '~/universal/types/enum'
import { RPC_ACTIONS } from '#/events/constants' // Electron modules
// Custom utilities and modules
import { configRouter } from './routes/config' import { configRouter } from './routes/config'
import { toolboxRouter } from './routes/toolbox' import { toolboxRouter } from './routes/toolbox'
import { systemRouter } from './routes/system' import { systemRouter } from './routes/system'
// Custom types/enums
import { IRPCActionType } from '~/universal/types/enum'
// External utility functions
import { RPC_ACTIONS } from '#/events/constants'
class RPCServer implements IRPCServer { class RPCServer implements IRPCServer {
private routes: IRPCRoutes = new Map() private routes: IRPCRoutes = new Map()
+7 -1
View File
@@ -1,6 +1,12 @@
import { IRPCActionType } from '~/universal/types/enum' import { IRPCActionType } from '~/universal/types/enum'
import { RPCRouter } from '../router' import { RPCRouter } from '../router'
import { deleteUploaderConfig, getUploaderConfigList, selectUploaderConfig, updateUploaderConfig, resetUploaderConfig } from '~/main/utils/handleUploaderConfig' import {
deleteUploaderConfig,
getUploaderConfigList,
selectUploaderConfig,
updateUploaderConfig,
resetUploaderConfig
} from '~/main/utils/handleUploaderConfig'
const configRouter = new RPCRouter() const configRouter = new RPCRouter()
+6 -1
View File
@@ -1,6 +1,11 @@
// External dependencies
import { app, clipboard, shell } from 'electron'
// Electron modules
// Custom utilities and modules
import { IRPCActionType } from '~/universal/types/enum' import { IRPCActionType } from '~/universal/types/enum'
import { RPCRouter } from '../router' import { RPCRouter } from '../router'
import { app, clipboard, shell } from 'electron'
const systemRouter = new RPCRouter() const systemRouter = new RPCRouter()
@@ -1,9 +1,18 @@
// External dependencies
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import path from 'path'
// Electron modules
// Custom utilities and modules
import { dbPathChecker, defaultConfigPath } from '~/main/apis/core/datastore/dbChecker' import { dbPathChecker, defaultConfigPath } from '~/main/apis/core/datastore/dbChecker'
import { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'
import { CLIPBOARD_IMAGE_FOLDER } from '~/universal/utils/static'
import { sendToolboxResWithType } from './utils' import { sendToolboxResWithType } from './utils'
// Custom types/enums
import { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'
// External utility functions
import { CLIPBOARD_IMAGE_FOLDER } from '~/universal/utils/static'
import { T } from '~/main/i18n' import { T } from '~/main/i18n'
const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD) const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD)
@@ -1,10 +1,19 @@
// External dependencies
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path'
// Electron modules
import { IpcMainEvent } from 'electron' import { IpcMainEvent } from 'electron'
import { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'
import { sendToolboxResWithType } from './utils' // Custom utilities and modules
import { dbPathChecker } from '~/main/apis/core/datastore/dbChecker' import { dbPathChecker } from '~/main/apis/core/datastore/dbChecker'
import { GalleryDB, DB_PATH } from '~/main/apis/core/datastore' import { GalleryDB, DB_PATH } from '~/main/apis/core/datastore'
import path from 'path' import { sendToolboxResWithType } from './utils'
// Custom types/enums
import { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'
// External utility functions
import { T } from '~/main/i18n' import { T } from '~/main/i18n'
export const checkFileMap: IToolboxCheckerMap< export const checkFileMap: IToolboxCheckerMap<
@@ -1,10 +1,21 @@
// External dependencies
import fs from 'fs-extra' import fs from 'fs-extra'
import { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'
import { sendToolboxResWithType } from './utils'
import tunnel from 'tunnel'
import { dbPathChecker } from '~/main/apis/core/datastore/dbChecker'
import { IConfig } from 'piclist'
import axios, { AxiosRequestConfig } from 'axios' import axios, { AxiosRequestConfig } from 'axios'
import tunnel from 'tunnel'
// Electron modules
// Custom utilities and modules
import { dbPathChecker } from '~/main/apis/core/datastore/dbChecker'
import { sendToolboxResWithType } from './utils'
// Custom types/enums
import { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'
// External utility functions
// Custom types/enums
import { IConfig } from 'piclist'
import { T } from '~/main/i18n' import { T } from '~/main/i18n'
const getProxy = (proxyStr: string): AxiosRequestConfig['proxy'] | false => { const getProxy = (proxyStr: string): AxiosRequestConfig['proxy'] | false => {
+5
View File
@@ -1,6 +1,11 @@
// External dependencies
import http from 'http' import http from 'http'
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import path from 'path'
// Electron modules
// Custom utilities and modules
import picgo from '@core/picgo' import picgo from '@core/picgo'
import logger from '../apis/core/picgo/logger' import logger from '../apis/core/picgo/logger'
+9
View File
@@ -1,7 +1,16 @@
// External dependencies
import yaml from 'js-yaml' import yaml from 'js-yaml'
import { ObjectAdapter, I18n } from '@picgo/i18n' import { ObjectAdapter, I18n } from '@picgo/i18n'
import path from 'path' import path from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
// Electron modules
// Custom utilities and modules
// Custom types/enums
// External utility functions
import { builtinI18nList } from '#/i18n' import { builtinI18nList } from '#/i18n'
class I18nManager { class I18nManager {
+16 -9
View File
@@ -15,10 +15,6 @@ import ipcList from '~/main/events/ipcList'
import busEventList from '~/main/events/busEventList' import busEventList from '~/main/events/busEventList'
import { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum' import { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum'
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
import {
updateShortKeyFromVersion212,
migrateGalleryFromVersion230
} from '~/main/migrate'
import { import {
uploadChoosedFiles, uploadChoosedFiles,
uploadClipboardFiles uploadClipboardFiles
@@ -29,7 +25,7 @@ import {
import server from '~/main/server/index' import server from '~/main/server/index'
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler' import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
import { getUploadFiles } from '~/main/utils/handleArgv' import { getUploadFiles } from '~/main/utils/handleArgv'
import db, { GalleryDB } from '~/main/apis/core/datastore' import db from '~/main/apis/core/datastore'
import bus from '@core/bus' import bus from '@core/bus'
import logger from 'apis/core/picgo/logger' import logger from 'apis/core/picgo/logger'
import picgo from 'apis/core/picgo' import picgo from 'apis/core/picgo'
@@ -62,9 +58,8 @@ const handleStartUpFiles = (argv: string[], cwd: string) => {
uploadChoosedFiles(win.webContents, files) uploadChoosedFiles(win.webContents, files)
} }
return true return true
} else {
return false
} }
return false
} }
autoUpdater.setFeedURL({ autoUpdater.setFeedURL({
@@ -90,9 +85,19 @@ autoUpdater.on('update-available', (info: UpdateInfo) => {
autoUpdater.downloadUpdate() autoUpdater.downloadUpdate()
} }
db.set('settings.showUpdateTip', !result.checkboxChecked) db.set('settings.showUpdateTip', !result.checkboxChecked)
}).catch((err) => {
logger.error(err)
}) })
}) })
autoUpdater.on('download-progress', (progressObj) => {
const percent = {
progress: progressObj.percent
}
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
window.webContents.send('updateProgress', percent)
})
autoUpdater.on('update-downloaded', () => { autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({ dialog.showMessageBox({
type: 'info', type: 'info',
@@ -100,9 +105,13 @@ autoUpdater.on('update-downloaded', () => {
buttons: ['Yes', 'No'], buttons: ['Yes', 'No'],
message: T('TIPS_UPDATE_DOWNLOADED') message: T('TIPS_UPDATE_DOWNLOADED')
}).then((result) => { }).then((result) => {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
window.webContents.send('updateProgress', { progress: 100 })
if (result.response === 0) { if (result.response === 0) {
autoUpdater.quitAndInstall() autoUpdater.quitAndInstall()
} }
}).catch((err) => {
logger.error(err)
}) })
}) })
@@ -122,8 +131,6 @@ class LifeCycle {
UpDownTaskQueue.getInstance() UpDownTaskQueue.getInstance()
manageIpcList.listen() manageIpcList.listen()
busEventList.listen() busEventList.listen()
updateShortKeyFromVersion212(db, db.get('settings.shortKey'))
await migrateGalleryFromVersion230(db, GalleryDB.getInstance(), picgo)
} }
private onReady () { private onReady () {
+117 -143
View File
@@ -1,18 +1,37 @@
// Axios
import axios from 'axios' import axios from 'axios'
// 加密函数、获取文件 MIME 类型、错误格式化函数、新的下载器、并发异步任务池
import { hmacSha1Base64, getFileMimeType, formatError, NewDownloader, ConcurrencyPromisePool } from '../utils/common' import { hmacSha1Base64, getFileMimeType, formatError, NewDownloader, ConcurrencyPromisePool } from '../utils/common'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron' import { ipcMain, IpcMainEvent } from 'electron'
// 快速 XML 解析器
import { XMLParser } from 'fast-xml-parser' import { XMLParser } from 'fast-xml-parser'
// 阿里云 OSS 客户端库
import OSS from 'ali-oss' import OSS from 'ali-oss'
// 路径处理库
import path from 'path' import path from 'path'
// 是否为图片的判断函数
import { isImage } from '~/renderer/manage/utils/common' import { isImage } from '~/renderer/manage/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum' import { IWindowList } from '#/types/enum'
import UpDownTaskQueue,
{ // 上传下载任务队列
uploadTaskSpecialStatus, import UpDownTaskQueue, { uploadTaskSpecialStatus, commonTaskStatus } from '../datastore/upDownTaskQueue'
commonTaskStatus
} from '../datastore/upDownTaskQueue' // 日志记录器
import { ManageLogger } from '../utils/logger' import { ManageLogger } from '../utils/logger'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static' import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
// 坑爹阿里云 返回数据类型标注和实际各种不一致 // 坑爹阿里云 返回数据类型标注和实际各种不一致
@@ -49,22 +68,20 @@ class AliyunApi {
} }
formatFile (item: OSS.ObjectMeta, slicedPrefix: string, urlPrefix: string): any { formatFile (item: OSS.ObjectMeta, slicedPrefix: string, urlPrefix: string): any {
const result = { const fileName = item.name.replace(slicedPrefix, '')
return {
...item, ...item,
key: item.name, key: item.name,
rawUrl: `${urlPrefix}/${item.name}`, fileName,
fileName: item.name.replace(slicedPrefix, ''),
fileSize: item.size, fileSize: item.size,
formatedTime: new Date(item.lastModified).toLocaleString(), formatedTime: new Date(item.lastModified).toLocaleString(),
isDir: false, isDir: false,
checked: false, checked: false,
match: false, match: false,
isImage: isImage(item.name.replace(slicedPrefix, '')) isImage: isImage(fileName),
rawUrl: item.url,
url: `${urlPrefix}/${item.name}`
} }
const temp = result.rawUrl
result.rawUrl = result.url
result.url = temp
return result
} }
getCanonicalizedOSSHeaders (headers: IStringKeyMap) { getCanonicalizedOSSHeaders (headers: IStringKeyMap) {
@@ -100,47 +117,30 @@ class AliyunApi {
* 获取存储桶列表 * 获取存储桶列表
*/ */
async getBucketList (): Promise<any> { async getBucketList (): Promise<any> {
const formatItem = (item: OSS.Bucket) => { const getBuckets = async (marker?: string) => {
return { const res = await this.ctx.listBuckets({
marker,
'max-keys': 1000
}) as IStringKeyMap
if (res?.res?.statusCode !== 200 || !res?.buckets) return { result: [], isTruncated: false }
const formattedBuckets = res.buckets.map((item: OSS.Bucket) => ({
Name: item.name, Name: item.name,
Location: item.region, Location: item.region,
CreationDate: item.creationDate CreationDate: item.creationDate
} }))
} return { result: formattedBuckets, isTruncated: res.isTruncated, nextMarker: res.nextMarker }
const res = await this.ctx.listBuckets({
'max-keys': 1000
}) as IStringKeyMap
const result = [] as IStringKeyMap[]
let NextMarker = ''
if (res.res.statusCode === 200) {
if (res.buckets) {
result.push(...res.buckets.map((item: OSS.Bucket) => formatItem(item)))
let isTruncated = res.isTruncated
NextMarker = res.nextMarker
while (isTruncated) {
const res = await this.ctx.listBuckets({
marker: NextMarker,
'max-keys': 1000
}) as IStringKeyMap
if (res.res.statusCode === 200) {
if (res.buckets) {
result.push(...res.buckets.map((item: OSS.Bucket) => formatItem(item)))
isTruncated = res.isTruncated
NextMarker = res.nextMarker
} else {
isTruncated = false
}
} else {
isTruncated = false
}
}
return result
} else {
return []
}
} else {
return []
} }
const result: IStringKeyMap[] = []
let NextMarker: string | undefined
let isTruncated: boolean
do {
const { result: buckets, isTruncated: truncated, nextMarker } = await getBuckets(NextMarker)
result.push(...buckets)
NextMarker = nextMarker
isTruncated = truncated
} while (isTruncated)
return result
} }
/** /**
@@ -151,6 +151,7 @@ class AliyunApi {
Date: new Date().toUTCString() Date: new Date().toUTCString()
} }
const authorization = this.authorization('GET', `/${param.bucketName}/?cname`, headers, '', '') const authorization = this.authorization('GET', `/${param.bucketName}/?cname`, headers, '', '')
const res = await axios({ const res = await axios({
url: `https://${param.bucketName}.${param.region}.aliyuncs.com/?cname`, url: `https://${param.bucketName}.${param.region}.aliyuncs.com/?cname`,
method: 'GET', method: 'GET',
@@ -159,25 +160,22 @@ class AliyunApi {
Authorization: authorization Authorization: authorization
} }
}) })
if (res.status === 200) {
if (res?.status === 200) {
const parser = new XMLParser() const parser = new XMLParser()
const result = parser.parse(res.data) const result = parser.parse(res.data)
if (result.ListCnameResult && result.ListCnameResult.Cname) {
if (Array.isArray(result.ListCnameResult.Cname)) { if (result.ListCnameResult?.Cname) {
const cnameList = [] as string[] const cnames = Array.isArray(result.ListCnameResult.Cname)
result.ListCnameResult.Cname.forEach((item: IStringKeyMap) => { ? result.ListCnameResult.Cname
item.Status === 'Enabled' && cnameList.push(item.Domain) : [result.ListCnameResult.Cname]
})
return cnameList return cnames
} else { .filter((item: IStringKeyMap) => item.Status === 'Enabled')
return result.ListCnameResult.Cname.Status === 'Enabled' ? [result.ListCnameResult.Cname.Domain] : [] .map((item: IStringKeyMap) => item.Domain)
}
} else {
return []
} }
} else {
return []
} }
return []
} }
/** /**
@@ -209,7 +207,7 @@ class AliyunApi {
dataRedundancyType: 'LRS', dataRedundancyType: 'LRS',
timeout: this.timeOut timeout: this.timeOut
}) })
return res && res.res.status === 200 return res?.res?.status === 200
} }
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> { async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
@@ -240,8 +238,8 @@ class AliyunApi {
}, { }, {
timeout: this.timeOut timeout: this.timeOut
}) })
if (res && res.res.statusCode === 200) { if (res?.res?.statusCode === 200) {
res.objects && res.objects.forEach((item: OSS.ObjectMeta) => { res?.objects?.forEach((item: OSS.ObjectMeta) => {
item.size !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) item.size !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
}) })
window.webContents.send(refreshDownloadFileTransferList, result) window.webContents.send(refreshDownloadFileTransferList, result)
@@ -288,11 +286,11 @@ class AliyunApi {
}, { }, {
timeout: this.timeOut timeout: this.timeOut
}) })
if (res && res.res.statusCode === 200) { if (res?.res?.statusCode === 200) {
res.prefixes && res.prefixes.forEach((item: string) => { res?.prefixes?.forEach((item: string) => {
result.fullList.push(this.formatFolder(item, slicedPrefix)) result.fullList.push(this.formatFolder(item, slicedPrefix))
}) })
res.objects && res.objects.forEach((item: OSS.ObjectMeta) => { res?.objects?.forEach((item: OSS.ObjectMeta) => {
item.size !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) item.size !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
}) })
window.webContents.send('refreshFileTransferList', result) window.webContents.send('refreshFileTransferList', result)
@@ -329,16 +327,10 @@ class AliyunApi {
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, marker, itemsPerPage } = configMap const { bucketName: bucket, bucketConfig: { Location: region }, prefix, marker, itemsPerPage } = configMap
const slicedPrefix = prefix.slice(1) const slicedPrefix = prefix.slice(1)
const urlPrefix = configMap.customUrl || `https://${bucket}.${region}.aliyuncs.com` const urlPrefix = configMap.customUrl || `https://${bucket}.${region}.aliyuncs.com`
let res = {} as any
const result = {
fullList: <any>[],
isTruncated: false,
nextMarker: '',
success: false
}
const client = this.getNewCtx(region, bucket) const client = this.getNewCtx(region, bucket)
res = await client.listV2({ const res = await client.listV2({
prefix: slicedPrefix === '' ? undefined : slicedPrefix, prefix: slicedPrefix || undefined,
delimiter: '/', delimiter: '/',
'max-keys': itemsPerPage.toString(), 'max-keys': itemsPerPage.toString(),
'continuation-token': marker 'continuation-token': marker
@@ -347,18 +339,24 @@ class AliyunApi {
}) as any }) as any
// prefixes can be null // prefixes can be null
// objects will be [] when no file // objects will be [] when no file
if (res && res.res.statusCode === 200) { if (res?.res.statusCode !== 200) {
res.prefixes && res.prefixes.forEach((item: string) => { return {
result.fullList.push(this.formatFolder(item, slicedPrefix)) fullList: [],
}) isTruncated: false,
res.objects && res.objects.forEach((item: OSS.ObjectMeta) => { nextMarker: '',
item.size !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) success: false
}) }
result.isTruncated = res.isTruncated }
result.nextMarker = res.nextContinuationToken || '' const fullList = [
result.success = true ...(res.prefixes?.map((item: string) => this.formatFolder(item, slicedPrefix)) || []),
...(res.objects?.filter((item: OSS.ObjectMeta) => item.size !== 0).map((item: OSS.ObjectMeta) => this.formatFile(item, slicedPrefix, urlPrefix)) || [])
]
return {
fullList,
isTruncated: res.isTruncated,
nextMarker: res.nextContinuationToken || '',
success: true
} }
return result
} }
/** /**
@@ -374,16 +372,15 @@ class AliyunApi {
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> { async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName, region, oldKey, newKey } = configMap const { bucketName, region, oldKey, newKey } = configMap
const client = this.getNewCtx(region, bucketName) const client = this.getNewCtx(region, bucketName)
const res = await client.copy( const copyRes = await client.copy(
newKey, newKey,
oldKey oldKey
) as any ) as any
if (res && res.res.statusCode === 200) { if (copyRes?.res.statusCode === 200) {
const res2 = await client.delete(oldKey) as any const deleteRes = await client.delete(oldKey) as any
return res2 && res2.res.statusCode === 204 return deleteRes?.res.statusCode === 204
} else {
return false
} }
return false
} }
/** /**
@@ -399,7 +396,7 @@ class AliyunApi {
const { bucketName, region, key } = configMap const { bucketName, region, key } = configMap
const client = this.getNewCtx(region, bucketName) 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.res.statusCode === 204 return res?.res.statusCode === 204
} }
/** /**
@@ -415,62 +412,39 @@ class AliyunApi {
CommonPrefixes: [] as any[], CommonPrefixes: [] as any[],
Contents: [] as any[] Contents: [] as any[]
} }
let res = await client.listV2({ do {
prefix: key, const res = await client.listV2({
delimiter: '/', prefix: key,
'max-keys': '1000' delimiter: '/',
}, { 'max-keys': '1000',
timeout: this.timeOut 'continuation-token': marker
}) as any }, {
if (res && res.res.statusCode === 200) { timeout: this.timeOut
}) as any
if (res?.res.statusCode !== 200) return false
res.prefixes !== null && allFileList.CommonPrefixes.push(...res.prefixes) res.prefixes !== null && allFileList.CommonPrefixes.push(...res.prefixes)
res.objects.length > 0 && allFileList.Contents.push(...res.objects) res.objects?.length > 0 && allFileList.Contents.push(...res.objects)
isTruncated = res.isTruncated isTruncated = res.isTruncated
marker = res.nextContinuationToken marker = res.nextContinuationToken
while (isTruncated) { } while (isTruncated)
res = await client.listV2({
prefix: key,
delimiter: '/',
'max-keys': '1000',
'continuation-token': marker
}, {
timeout: this.timeOut
}) as any
if (res && res.res.statusCode === 200) {
res.prefixes !== null && allFileList.CommonPrefixes.push(...res.prefixes)
res.objects.length > 0 && allFileList.Contents.push(...res.objects)
isTruncated = res.isTruncated
marker = res.nextContinuationToken
} else {
return false
}
}
} else {
return false
}
if (allFileList.CommonPrefixes.length > 0) { if (allFileList.CommonPrefixes.length > 0) {
for (const item of allFileList.CommonPrefixes) { for (const item of allFileList.CommonPrefixes) {
res = await this.deleteBucketFolder({ const successfully = await this.deleteBucketFolder({
bucketName, bucketName,
region, region,
key: item key: item
}) })
if (!res) { if (!successfully) return false
return false
}
} }
} }
if (allFileList.Contents.length > 0) { if (allFileList.Contents.length > 0) {
const cycle = Math.ceil(allFileList.Contents.length / 1000) const cycle = Math.ceil(allFileList.Contents.length / 1000)
for (let i = 0; i < cycle; i++) { for (let i = 0; i < cycle; i++) {
res = await client.deleteMulti( const deleteRes = await client.deleteMulti(
allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => { allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => item.name)) as any
return item.name if (deleteRes?.res.statusCode !== 200) return false
})
) as any
if (!(res && res.res.statusCode === 200)) {
return false
}
} }
} }
return true return true
@@ -493,7 +467,7 @@ class AliyunApi {
const res = client.signatureUrl(key, { const res = client.signatureUrl(key, {
expires: expires || 3600 expires: expires || 3600
}) })
return customUrl ? `${customUrl.replace(/\/$/, '')}/${key}${res.slice(res.indexOf('?'))}` : res return customUrl ? `${customUrl.replace(/\/+$/, '')}/${key}${res.slice(res.indexOf('?'))}` : res
} }
/** /**
@@ -547,7 +521,7 @@ class AliyunApi {
} }
).then((res: any) => { ).then((res: any) => {
const id = `${bucketName}-${region}-${key}-${filePath}` const id = `${bucketName}-${region}-${key}-${filePath}`
if (res && res.res.statusCode === 200) { if (res?.res?.statusCode === 200) {
instance.updateUploadTask({ instance.updateUploadTask({
id, id,
progress: 100, progress: 100,
@@ -587,7 +561,7 @@ class AliyunApi {
const { bucketName, region, key } = configMap const { bucketName, region, key } = configMap
const client = this.getNewCtx(region, bucketName) 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.res.statusCode === 200 return res?.res?.statusCode === 200
} }
/** /**
+12 -8
View File
@@ -1,21 +1,25 @@
import TcyunApi from './tcyun'
import AliyunApi from './aliyun' import AliyunApi from './aliyun'
import QiniuApi from './qiniu'
import UpyunApi from './upyun'
import SmmsApi from './smms'
import GithubApi from './github' import GithubApi from './github'
import ImgurApi from './imgur' import ImgurApi from './imgur'
import LocalApi from './local'
import QiniuApi from './qiniu'
import S3plistApi from './s3plist' import S3plistApi from './s3plist'
import SftpApi from './sftp'
import SmmsApi from './smms'
import TcyunApi from './tcyun'
import UpyunApi from './upyun'
import WebdavplistApi from './webdavplist' import WebdavplistApi from './webdavplist'
export default { export default {
TcyunApi,
AliyunApi, AliyunApi,
QiniuApi,
UpyunApi,
SmmsApi,
GithubApi, GithubApi,
ImgurApi, ImgurApi,
LocalApi,
QiniuApi,
S3plistApi, S3plistApi,
SftpApi,
SmmsApi,
TcyunApi,
UpyunApi,
WebdavplistApi WebdavplistApi
} }
+54 -66
View File
@@ -1,16 +1,34 @@
// HTTP 请求库
import got from 'got' import got from 'got'
// 日志记录器
import { ManageLogger } from '../utils/logger' import { ManageLogger } from '../utils/logger'
// HTTP 代理格式化函数、是否为图片的判断函数
import { formatHttpProxy, isImage } from '~/renderer/manage/utils/common' import { formatHttpProxy, isImage } from '~/renderer/manage/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum' import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron' import { ipcMain, IpcMainEvent } from 'electron'
// got 上传函数、路径处理函数、新的下载器、获取请求代理、获取请求选项、并发异步任务池、错误格式化函数
import { gotUpload, trimPath, NewDownloader, getAgent, getOptions, ConcurrencyPromisePool, formatError } from '../utils/common' import { gotUpload, trimPath, NewDownloader, getAgent, getOptions, ConcurrencyPromisePool, formatError } from '../utils/common'
import UpDownTaskQueue,
{ // 上传下载任务队列
commonTaskStatus import UpDownTaskQueue, { commonTaskStatus } from '../datastore/upDownTaskQueue'
} from '../datastore/upDownTaskQueue'
// 文件系统库
import fs from 'fs-extra' import fs from 'fs-extra'
// 路径处理库
import path from 'path' import path from 'path'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static' import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class GithubApi { class GithubApi {
@@ -35,12 +53,7 @@ class GithubApi {
} }
formatFolder (item: any, slicedPrefix: string) { formatFolder (item: any, slicedPrefix: string) {
let key = '' const key = `${slicedPrefix ? `${slicedPrefix}/` : ''}${item.path}/`
if (slicedPrefix === '') {
key = `${item.path}/`
} else {
key = `${slicedPrefix}/${item.path}/`
}
return { return {
...item, ...item,
Key: key, Key: key,
@@ -57,27 +70,18 @@ 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 = '' let rawUrl = ''
if (cdnUrl) { const placeholders = ['{username}', '{repo}', '{branch}', '{path}']
const placeholder = ['{username}', '{repo}', '{branch}', '{path}'] const key = slicedPrefix === '' ? item.path : `${slicedPrefix}/${item.path}`
if (placeholder.some(item => cdnUrl.includes(item))) { rawUrl = cdnUrl
rawUrl = cdnUrl.replace('{username}', this.username) ? placeholders.some(item => cdnUrl.includes(item))
.replace('{repo}', repo) ? placeholders.reduce((url, ph) => {
.replace('{branch}', branch) const value = ph === '{username}' ? this.username : ph === '{repo}' ? repo : ph === '{branch}' ? branch : ph === '{path}' ? `${slicedPrefix}/${item.path}` : ''
.replace('{path}', `${slicedPrefix}/${item.path}`) return url.replaceAll(ph, value)
} else { }, cdnUrl)
rawUrl = `${cdnUrl}/${slicedPrefix}/${item.path}` : `${cdnUrl}/${key}`
} : `https://raw.githubusercontent.com/${this.username}/${repo}/${branch}/${key}`
} else {
rawUrl = `https://raw.githubusercontent.com/${this.username}/${repo}/${branch}/${slicedPrefix}/${item.path}`
}
rawUrl = rawUrl.replace(/(?<!https?:)\/{2,}/g, '/') rawUrl = rawUrl.replace(/(?<!https?:)\/{2,}/g, '/')
let key = '' return {
if (slicedPrefix === '') {
key = item.path
} else {
key = `${slicedPrefix}/${item.path}`
}
const result = {
...item, ...item,
Key: key, Key: key,
key, key,
@@ -88,12 +92,9 @@ class GithubApi {
checked: false, checked: false,
match: false, match: false,
isImage: isImage(item.path), isImage: isImage(item.path),
rawUrl rawUrl: item.url,
url: rawUrl
} }
const temp = result.rawUrl
result.rawUrl = result.url
result.url = temp
return result
} }
/** /**
@@ -151,7 +152,7 @@ class GithubApi {
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> { async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)! const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const { bucketName: repo, customUrl: branch, prefix, cancelToken, cdnUrl } = configMap const { bucketName: repo, customUrl: branch, prefix, cancelToken, cdnUrl } = configMap
const slicedPrefix = prefix.replace(/^\//, '').replace(/\/$/, '') const slicedPrefix = prefix.replace(/(^\/+|\/+$)/g, '')
const cancelTask = [false] const cancelTask = [false]
ipcMain.on(cancelDownloadLoadingFileList, (_evt: IpcMainEvent, token: string) => { ipcMain.on(cancelDownloadLoadingFileList, (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) { if (token === cancelToken) {
@@ -202,7 +203,7 @@ class GithubApi {
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> { async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)! const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const { bucketName: repo, customUrl: branch, prefix, cancelToken, cdnUrl } = configMap const { bucketName: repo, customUrl: branch, prefix, cancelToken, cdnUrl } = configMap
const slicedPrefix = prefix.replace(/^\//, '').replace(/\/$/, '') const slicedPrefix = prefix.replace(/(^\/+|\/+$)/g, '')
const cancelTask = [false] const cancelTask = [false]
ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) { if (token === cancelToken) {
@@ -269,37 +270,35 @@ class GithubApi {
*/ */
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> { async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName: repo, githubBranch: branch, key } = configMap 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}`, `${this.baseUrl}/repos/${this.username}/${repo}/git/refs/heads/${branch}`,
getOptions('GET', this.commonHeaders, undefined, 'json', undefined, undefined, this.proxy) getOptions('GET', this.commonHeaders, undefined, 'json', undefined, undefined, this.proxy)
) as any ) as any
if (refRes.statusCode !== 200) { if (refRes.statusCode !== 200) return false
return false
}
const refSha = refRes.body.object.sha 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}`, `${this.baseUrl}/repos/${this.username}/${repo}/branches/${branch}`,
getOptions('GET', undefined, undefined, 'json', undefined, undefined, this.proxy) getOptions('GET', undefined, undefined, 'json', undefined, undefined, this.proxy)
) as any ) as any
if (rootRes.statusCode !== 200) { if (rootRes.statusCode !== 200) return false
return false
}
const rootSha = rootRes.body.commit.commit.tree.sha const rootSha = rootRes.body.commit.commit.tree.sha
// TODO: if there are more than 10000 files in the folder, it will be truncated // TODO: if there are more than 10000 files in the folder, it will be truncated
// Rare cases, not considered for now // 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(/^\//, '').replace(/\/$/, '')}`, `${this.baseUrl}/repos/${this.username}/${repo}/git/trees/${branch}:${key.replace(/(^\/+|\/+$)/g, '')}`,
getOptions('GET', this.commonHeaders, { getOptions('GET', this.commonHeaders, {
recursive: true recursive: true
}, 'json', undefined, undefined, this.proxy) }, 'json', undefined, undefined, this.proxy)
) as any ) as any
if (treeRes.statusCode !== 200) { if (treeRes.statusCode !== 200) return false
return false
}
const oldTree = treeRes.body.tree const oldTree = treeRes.body.tree
// create a new tree
const newTree = oldTree.filter((item: any) => item.type === 'blob') const newTree = oldTree.filter((item: any) => item.type === 'blob')
.map((item:any) => ({ .map((item:any) => ({
path: `${key.replace(/^\//, '').replace(/\/$/, '')}/${item.path}`, path: `${key.replace(/(^\/+|\/+$)/g, '')}/${item.path}`,
mode: item.mode, mode: item.mode,
type: item.type, type: item.type,
sha: null sha: null
@@ -311,10 +310,9 @@ class GithubApi {
tree: newTree tree: newTree
}), undefined, this.proxy) }), undefined, this.proxy)
) as any ) as any
if (newTreeShaRes.statusCode !== 201) { if (newTreeShaRes.statusCode !== 201) return false
return false
}
const newTreeSha = newTreeShaRes.body.sha 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`, `${this.baseUrl}/repos/${this.username}/${repo}/git/commits`,
getOptions('POST', this.commonHeaders, undefined, 'json', JSON.stringify({ getOptions('POST', this.commonHeaders, undefined, 'json', JSON.stringify({
@@ -323,20 +321,16 @@ class GithubApi {
parents: [refSha] parents: [refSha]
}), undefined, this.proxy) }), undefined, this.proxy)
) as any ) as any
if (commitRes.statusCode !== 201) { if (commitRes.statusCode !== 201) return false
return false
}
const commitSha = commitRes.body.sha 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}`, `${this.baseUrl}/repos/${this.username}/${repo}/git/refs/heads/${branch}`,
getOptions('PATCH', this.commonHeaders, undefined, 'json', JSON.stringify({ getOptions('PATCH', this.commonHeaders, undefined, 'json', JSON.stringify({
sha: commitSha sha: commitSha
}), undefined, this.proxy) }), undefined, this.proxy)
) as any ) as any
if (updateRefRes.statusCode !== 200) { return updateRefRes.statusCode === 200
return false
}
return true
} }
/** /**
@@ -352,20 +346,14 @@ class GithubApi {
*/ */
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> { async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
const { bucketName: repo, customUrl: branch, key, rawUrl, githubPrivate: isPrivate } = configMap const { bucketName: repo, customUrl: branch, key, rawUrl, githubPrivate: isPrivate } = configMap
if (!isPrivate) { if (!isPrivate) return rawUrl
return rawUrl
}
const res = await got( const res = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/contents/${key}`, `${this.baseUrl}/repos/${this.username}/${repo}/contents/${key}`,
getOptions('GET', this.commonHeaders, { getOptions('GET', this.commonHeaders, {
ref: branch ref: branch
}, 'json', undefined, undefined, this.proxy) }, 'json', undefined, undefined, this.proxy)
) as any ) as any
if (res.statusCode === 200) { return res.statusCode === 200 ? res.body.download_url : ''
return res.body.download_url
} else {
return ''
}
} }
/** /**
+34 -26
View File
@@ -1,17 +1,27 @@
import got from 'got' // External dependencies
import ManageLogger from '../utils/logger'
import { getAgent, getOptions, NewDownloader, gotUpload, getFileMimeType, ConcurrencyPromisePool, formatError } from '../utils/common'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import { ipcMain, IpcMainEvent } from 'electron'
import { formatHttpProxy, isImage } from '~/renderer/manage/utils/common'
import path from 'path'
import UpDownTaskQueue,
{
commonTaskStatus
} from '../datastore/upDownTaskQueue'
import FormData from 'form-data'
import fs from 'fs-extra' import fs from 'fs-extra'
import FormData from 'form-data'
import got from 'got'
import path from 'path'
// Electron modules
import { ipcMain, IpcMainEvent } from 'electron'
// Custom utilities and modules
import { IWindowList } from '#/types/enum'
import {
ConcurrencyPromisePool,
formatError,
getFileMimeType,
getOptions,
getAgent,
gotUpload,
NewDownloader
} from '../utils/common'
import ManageLogger from '../utils/logger'
import windowManager from 'apis/app/window/windowManager'
import { formatHttpProxy, isImage } from '~/renderer/manage/utils/common'
import UpDownTaskQueue, { commonTaskStatus } from '../datastore/upDownTaskQueue'
class ImgurApi { class ImgurApi {
userName: string userName: string
@@ -35,17 +45,19 @@ class ImgurApi {
} }
formatFile (item: any) { formatFile (item: any) {
const fileName = path.basename(item.link)
const isImg = isImage(fileName)
return { return {
...item, ...item,
Key: path.basename(item.link), Key: fileName,
key: path.basename(item.link), key: fileName,
fileName: `${item.name}${path.extname(item.link)}`, fileName: `${item.name}${path.extname(item.link)}`,
formatedTime: new Date(item.datetime * 1000).toLocaleString(), formatedTime: new Date(item.datetime * 1000).toLocaleString(),
fileSize: item.size, fileSize: item.size,
isDir: false, isDir: false,
checked: false, checked: false,
match: false, match: false,
isImage: isImage(path.basename(item.link)), isImage: isImg,
url: item.link, url: item.link,
sha: item.deletehash sha: item.deletehash
} }
@@ -69,16 +81,12 @@ class ImgurApi {
result.push(...res.body.data) result.push(...res.body.data)
initPage++ initPage++
} while (res.body.data.length > 0) } while (res.body.data.length > 0)
const finalResult = [] as any[] const finalResult = result.map((item: any) => ({
for (let i = 0; i < result.length; i++) { ...item,
const item = result[i] Name: item.title,
finalResult.push({ Location: item.id,
...item, CreationDate: item.datetime
Name: item.title, })) as any[]
Location: item.id,
CreationDate: item.datetime
})
}
finalResult.push({ finalResult.push({
Name: '全部', Name: '全部',
Location: 'unclassified', Location: 'unclassified',
+321
View File
@@ -0,0 +1,321 @@
// 日志记录器
import ManageLogger from '../utils/logger'
// 错误格式化函数、端点地址格式化函数、获取内部代理、新的下载器、并发异步任务池
import { formatError } from '../utils/common'
// HTTP 代理格式化函数、是否为图片的判断函数
import { isImage } from '@/manage/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron'
// 上传下载任务队列
import UpDownTaskQueue, { uploadTaskSpecialStatus, commonTaskStatus, downloadTaskSpecialStatus } from '../datastore/upDownTaskQueue'
// 文件系统库
import fs from 'fs-extra'
// 路径处理库
import path from 'path'
import * as fsWalk from '@nodelib/fs.walk'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class LocalApi {
logger: ManageLogger
isWindows: boolean
constructor (logger: ManageLogger) {
this.logger = logger
this.isWindows = process.platform === 'win32'
}
logParam = (error:any, method: string) =>
this.logger.error(formatError(error, { class: 'LocalApi', method }))
// windows 系统下将路径转换为 unix 风格
transPathToUnix (filePath: string | undefined) {
if (!filePath) return ''
return this.isWindows ? filePath.split(path.sep).join(path.posix.sep) : filePath.replace(/^\/+/, '')
}
transBack (filePath: string | undefined) {
if (!filePath) return ''
return this.isWindows
? filePath.split(path.posix.sep).join(path.sep).replace(/^\\+|\\+$/g, '')
: `/${filePath.replace(/^\/+|\/+$/g, '')}`
}
formatFolder (item: fs.Stats, urlPrefix: string, fileName: string, filePath: string) {
const key = `${this.transPathToUnix(filePath)}/`.replace(/\/+$/, '/')
return {
...item,
key,
fileName,
fileSize: 0,
Key: key,
formatedTime: '',
isDir: true,
checked: false,
isImage: false,
match: false,
url: urlPrefix
}
}
formatFile (item: fs.Stats, urlPrefix: string, fileName: string, filePath: string, isDownload = false) {
const key = isDownload ? filePath : this.transPathToUnix(filePath)
return {
...item,
key,
fileName,
fileSize: item.size,
Key: key,
formatedTime: new Date(item.mtime).toLocaleString(),
isDir: false,
checked: false,
match: false,
isImage: isImage(fileName),
url: urlPrefix
}
}
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const { prefix, customUrl = '', cancelToken } = configMap
const urlPrefix = customUrl.replace(/\/+$/, '')
const cancelTask = [false]
ipcMain.on(cancelDownloadLoadingFileList, (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) {
cancelTask[0] = true
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
}
})
let res = {} as any
const result = {
fullList: <any>[],
success: false,
finished: false
}
try {
res = fsWalk.walkSync(this.transBack(prefix), {
followSymbolicLinks: true,
fs,
stats: true,
throwErrorOnBrokenSymbolicLink: false
})
if (res.length) {
result.fullList.push(
...res
.filter((item: fsWalk.Entry) => item.stats?.isFile())
.map((item: any) => this.formatFile(item, urlPrefix, item.name, item.path, true))
)
result.success = true
}
} catch (error) {
this.logParam(error, 'getBucketListRecursively')
}
result.finished = true
window.webContents.send(refreshDownloadFileTransferList, result)
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
}
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const { customUrl = '', cancelToken, baseDir } = configMap
let prefix = configMap.prefix
prefix = this.transBack(prefix)
const urlPrefix = customUrl.replace(/\/+$/, '')
let webPath = configMap.webPath || ''
if (webPath && customUrl && webPath !== '/') {
webPath = webPath.replace(/^\/+|\/+$/, '')
}
const cancelTask = [false]
ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) {
cancelTask[0] = true
ipcMain.removeAllListeners('cancelLoadingFileList')
}
})
const result = {
fullList: <any>[],
success: false,
finished: false
}
try {
const res = await fs.readdir(prefix, {
withFileTypes: true
})
if (res.length) {
let urlPrefixF
res.forEach((item: fs.Dirent) => {
const pathOfFile = path.join(prefix, item.name)
let relative
if (customUrl) {
const relativePath = path.relative(this.transBack(baseDir), pathOfFile)
relative = urlPrefix + `/${path.join(webPath, relativePath)}`.replace(/\\/g, '/').replace(/\/+/g, '/')
urlPrefixF = this.isWindows ? relative.replace(/\/[a-zA-Z]:\//, '/') : relative
} else {
urlPrefixF = pathOfFile
}
const stats = fs.statSync(pathOfFile)
if (item.isDirectory()) {
result.fullList.push(this.formatFolder(stats, urlPrefixF, item.name, pathOfFile))
} else {
result.fullList.push(this.formatFile(stats, urlPrefixF, item.name, pathOfFile))
}
})
result.success = true
}
} catch (error) {
this.logParam(error, 'getBucketListBackstage')
}
result.finished = true
window.webContents.send('refreshFileTransferList', result)
ipcMain.removeAllListeners('cancelLoadingFileList')
}
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { oldKey, newKey } = configMap
let result = false
try {
await fs.rename(this.transBack(oldKey), this.transBack(newKey))
result = true
} catch (error) {
this.logParam(error, 'renameBucketFile')
}
return result
}
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { key } = configMap
let result = false
try {
await fs.remove(this.transBack(key))
result = true
} catch (error) {
this.logParam(error, 'deleteBucketFile')
}
return result
}
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { key } = configMap
let result = false
try {
await fs.rm(this.transBack(key), {
recursive: true
})
result = true
} catch (error) {
this.logParam(error, 'deleteBucketFolder')
}
return result
}
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { fileArray } = configMap
const instance = UpDownTaskQueue.getInstance()
for (const item of fileArray) {
const { alias, bucketName, key, filePath, fileName } = item
const id = `${alias}-${bucketName}-${key}-${filePath}`
if (instance.getUploadTask(id)) {
continue
}
instance.addUploadTask({
id,
progress: 0,
status: commonTaskStatus.queuing,
sourceFileName: fileName,
sourceFilePath: filePath,
targetFilePath: key,
targetFileBucket: bucketName,
targetFileRegion: '',
noProgress: true
})
try {
fs.ensureFileSync(this.transBack(key))
await fs.copyFile(filePath, this.transBack(key))
instance.updateUploadTask({
id,
progress: 100,
status: uploadTaskSpecialStatus.uploaded,
finishTime: new Date().toLocaleString()
})
} catch (error) {
this.logParam(error, 'uploadBucketFile')
instance.updateUploadTask({
id,
progress: 0,
status: commonTaskStatus.failed,
finishTime: new Date().toLocaleString()
})
}
}
return true
}
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { key } = configMap
let result = false
try {
await fs.mkdir(this.transBack(key), {
recursive: true
})
result = true
} catch (error) {
this.logParam(error, 'createBucketFolder')
}
return result
}
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { downloadPath, fileArray } = configMap
const instance = UpDownTaskQueue.getInstance()
for (const item of fileArray) {
const { alias, bucketName, key, fileName } = item
const savedFilePath = path.join(downloadPath, fileName.replace(/[:*?"<>|]/g, ''))
const id = `${alias}-${bucketName}-local-${key}`
if (instance.getDownloadTask(id)) {
continue
}
instance.addDownloadTask({
id,
progress: 0,
status: commonTaskStatus.queuing,
sourceFileName: fileName,
targetFilePath: savedFilePath
})
try {
fs.ensureFileSync(savedFilePath)
await fs.copyFile(this.transBack(key), savedFilePath)
instance.updateDownloadTask({
id,
progress: 100,
status: downloadTaskSpecialStatus.downloaded,
finishTime: new Date().toLocaleString()
})
} catch (error) {
this.logParam(error, 'downloadBucketFile')
instance.updateDownloadTask({
id,
progress: 0,
status: commonTaskStatus.failed,
finishTime: new Date().toLocaleString()
})
}
}
return true
}
}
export default LocalApi
+69 -80
View File
@@ -1,17 +1,34 @@
// Axios
import axios from 'axios' import axios from 'axios'
// 加密函数、获取文件 MIME 类型、新的下载器、错误格式化函数、并发异步任务池
import { hmacSha1Base64, getFileMimeType, NewDownloader, formatError, ConcurrencyPromisePool } from '../utils/common' import { hmacSha1Base64, getFileMimeType, NewDownloader, formatError, ConcurrencyPromisePool } from '../utils/common'
// 七牛云客户端库
import qiniu from 'qiniu/index' import qiniu from 'qiniu/index'
// 路径处理库
import path from 'path' import path from 'path'
// 是否为图片的判断函数
import { isImage } from '~/renderer/manage/utils/common' import { isImage } from '~/renderer/manage/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum' import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron' import { ipcMain, IpcMainEvent } from 'electron'
import UpDownTaskQueue,
{ // 上传下载任务队列
uploadTaskSpecialStatus, import UpDownTaskQueue, { uploadTaskSpecialStatus, commonTaskStatus } from '../datastore/upDownTaskQueue'
commonTaskStatus
} from '../datastore/upDownTaskQueue' // 日志记录器
import { ManageLogger } from '../utils/logger' import { ManageLogger } from '../utils/logger'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static' import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class QiniuApi { class QiniuApi {
@@ -49,16 +66,17 @@ class QiniuApi {
} }
formatFile (item: any, slicedPrefix: string, urlPrefix: string) { formatFile (item: any, slicedPrefix: string, urlPrefix: string) {
const fileName = item.key.replace(slicedPrefix, '')
return { return {
...item, ...item,
fileName: item.key.replace(slicedPrefix, ''), fileName,
url: `${urlPrefix}/${item.key}`, url: `${urlPrefix}/${item.key}`,
fileSize: item.fsize, fileSize: item.fsize,
formatedTime: new Date(parseInt(item.putTime.toString().slice(0, -4), 10)).toLocaleString(), formatedTime: new Date(parseInt(item.putTime.toString().slice(0, -4), 10)).toLocaleString(),
isDir: false, isDir: false,
checked: false, checked: false,
match: false, match: false,
isImage: isImage(item.key.replace(slicedPrefix, '')) isImage: isImage(fileName)
} }
} }
@@ -71,22 +89,20 @@ class QiniuApi {
contentType: string, contentType: string,
xQiniuHeaders?: IStringKeyMap xQiniuHeaders?: IStringKeyMap
) { ) {
let signStr = `${method.toUpperCase()} ${urlPath}` let signStr = `${method.toUpperCase()} ${urlPath}${query ? `?${query}` : ''}\nHost: ${host}`
query && (signStr += `?${query}`)
signStr += `\nHost: ${host}`
contentType && (signStr += `\nContent-Type: ${contentType}`) contentType && (signStr += `\nContent-Type: ${contentType}`)
let xQiniuHeaderStr = ''
if (xQiniuHeaders) { if (xQiniuHeaders) {
const xQiniuHeaderKeys = Object.keys(xQiniuHeaders).sort() const xQiniuHeaderStr = Object.keys(xQiniuHeaders)
xQiniuHeaderKeys.forEach((key) => { .sort()
xQiniuHeaderStr += `\n${key}:${xQiniuHeaders[key]}` .map((key) => `\n${key}:${xQiniuHeaders[key]}`)
}) .join('')
signStr += xQiniuHeaderStr signStr += xQiniuHeaderStr
} }
signStr += '\n\n' signStr += '\n\n'
if (contentType !== 'application/octet-stream' && body) {
signStr += body if (contentType !== 'application/octet-stream' && body) signStr += body
}
return `Qiniu ${this.accessKey}:${hmacSha1Base64(this.secretKey, signStr).replace(/\+/g, '-').replace(/\//g, '_')}` return `Qiniu ${this.accessKey}:${hmacSha1Base64(this.secretKey, signStr).replace(/\+/g, '-').replace(/\//g, '_')}`
} }
@@ -103,28 +119,21 @@ class QiniuApi {
}, },
timeout: this.timeout timeout: this.timeout
}) })
if (res && res.status === 200) { if (res?.status === 200 && res?.data?.length) {
if (res.data && res.data.length) { const result = [] as any[]
const result = [] as any[] for (let i = 0; i < res.data.length; i++) {
for (let i = 0; i < res.data.length; i++) { const info = await this.getBucketInfo({ bucketName: res.data[i] })
const info = await this.getBucketInfo({ bucketName: res.data[i] }) if (!info.success) return []
if (!info.success) { result.push({
return [] Name: res.data[i],
} Location: info.zone,
result.push({ CreationDate: new Date().toISOString(),
Name: res.data[i], Private: info.private
Location: info.zone, })
CreationDate: new Date().toISOString(),
Private: info.private
})
}
return result
} else {
return []
} }
} else { return result
return []
} }
return []
} }
/** /**
@@ -148,17 +157,15 @@ class QiniuApi {
}, },
timeout: this.timeout timeout: this.timeout
}) })
if (res && res.status === 200) { return res?.status === 200
return { ? {
success: true, success: true,
private: res.data.private, private: res.data.private,
zone: res.data.zone zone: res.data.zone
} }
} else { : {
return {
success: false success: false
} }
}
} }
/** /**
@@ -178,11 +185,7 @@ class QiniuApi {
}, },
timeout: this.timeout timeout: this.timeout
}) })
if (res && res.status === 200) { return res?.status === 200 && res?.data?.length ? res.data : []
return res.data && res.data.length ? res.data : []
} else {
return []
}
} }
/** /**
@@ -209,7 +212,7 @@ class QiniuApi {
}, },
timeout: this.timeout timeout: this.timeout
}) })
return res && res.status === 200 return res?.status === 200
} }
/** /**
@@ -222,8 +225,7 @@ class QiniuApi {
* } * }
*/ */
async createBucket (configMap: IStringKeyMap): Promise<boolean> { async createBucket (configMap: IStringKeyMap): Promise<boolean> {
const { BucketName, region } = configMap const { BucketName, region, acl } = configMap
const { acl } = configMap
const urlPath = `/mkbucketv3/${BucketName}/region/${region}` const urlPath = `/mkbucketv3/${BucketName}/region/${region}`
const authorization = this.authorization('POST', urlPath, this.host, '', '', 'application/json') const authorization = this.authorization('POST', urlPath, this.host, '', '', 'application/json')
const res = await axios({ const res = await axios({
@@ -236,15 +238,12 @@ class QiniuApi {
}, },
timeout: this.timeout timeout: this.timeout
}) })
if (res && res.status === 200) { return res?.status === 200
const changeAclRes = await this.setBucketAclPolicy({ ? await this.setBucketAclPolicy({
bucketName: BucketName, bucketName: BucketName,
isPrivate: !acl isPrivate: !acl
}) })
return changeAclRes : false
} else {
return false
}
} }
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> { async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
@@ -407,19 +406,19 @@ class QiniuApi {
} }
}) })
}) })
if (res && res.respInfo.statusCode === 200) { if (res?.respInfo?.statusCode === 200) {
if (res.respBody && res.respBody.commonPrefixes) { if (res.respBody?.commonPrefixes) {
res.respBody.commonPrefixes.forEach((item: string) => { res.respBody.commonPrefixes.forEach((item: string) => {
result.fullList.push(this.formatFolder(item, slicedPrefix)) result.fullList.push(this.formatFolder(item, slicedPrefix))
}) })
} }
if (res.respBody && res.respBody.items) { if (res.respBody?.items) {
res.respBody.items.forEach((item: any) => { res.respBody.items.forEach((item: any) => {
item.fsize !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) item.fsize !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
}) })
} }
result.isTruncated = !!(res.respBody && res.respBody.marker) result.isTruncated = !!(res.respBody?.marker)
result.nextMarker = res.respBody && res.respBody.marker ? res.respBody.marker : '' result.nextMarker = res.respBody?.marker ? res.respBody.marker : ''
result.success = true result.success = true
} }
return result return result
@@ -450,11 +449,7 @@ class QiniuApi {
} }
}) })
}) as any }) as any
if (res && res.respInfo.statusCode === 200) { return res?.respInfo?.statusCode === 200
return true
} else {
return false
}
} }
/** /**
@@ -487,12 +482,12 @@ class QiniuApi {
} }
}) })
}) as any }) as any
if (res && res.respInfo.statusCode === 200) { if (res?.respInfo?.statusCode === 200) {
if (res.respBody && res.respBody.items) { if (res.respBody?.items) {
allFileList.Contents = allFileList.Contents.concat(res.respBody.items) allFileList.Contents = allFileList.Contents.concat(res.respBody.items)
} }
isTruncated = !!(res.respBody && res.respBody.marker) isTruncated = !!(res.respBody?.marker)
marker = res.respBody && res.respBody.marker ? res.respBody.marker : '' marker = res.respBody?.marker ? res.respBody.marker : ''
} else { } else {
return false return false
} }
@@ -514,9 +509,7 @@ class QiniuApi {
} }
}) })
}) as any }) as any
if (!(res && res.respInfo.statusCode === 200)) { if (res?.respInfo?.statusCode !== 200) return false
return false
}
} }
return true return true
} }
@@ -549,7 +542,7 @@ class QiniuApi {
} }
}) })
}) as any }) as any
return res && res.respInfo.statusCode === 200 return res?.respInfo?.statusCode === 200
} }
/** /**
@@ -671,11 +664,7 @@ class QiniuApi {
} }
}) })
}) as any }) as any
if (res && res.respInfo.statusCode === 200) { return res?.respInfo?.statusCode === 200
return true
} else {
return false
}
} }
/** /**
+182 -107
View File
@@ -1,9 +1,9 @@
// AWS S3 相关
import { import {
S3Client, S3Client,
ListBucketsCommand, ListBucketsCommand,
ListObjectsV2Command, ListObjectsV2Command,
GetBucketLocationCommand, GetBucketLocationCommand,
S3ClientConfig,
_Object, _Object,
CommonPrefix, CommonPrefix,
ListObjectsV2CommandOutput, ListObjectsV2CommandOutput,
@@ -11,46 +11,63 @@ import {
GetObjectCommand, GetObjectCommand,
DeleteObjectCommand, DeleteObjectCommand,
DeleteObjectsCommand, DeleteObjectsCommand,
PutObjectCommand PutObjectCommand,
S3ClientConfig
} from '@aws-sdk/client-s3' } from '@aws-sdk/client-s3'
// AWS S3 上传和进度
import { Upload, Progress } from '@aws-sdk/lib-storage' import { Upload, Progress } from '@aws-sdk/lib-storage'
// AWS S3 请求签名
import { getSignedUrl } from '@aws-sdk/s3-request-presigner' import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
// HTTP 和 HTTPS 模块
import https from 'https' import https from 'https'
import http from 'http' import http, { AgentOptions } from 'http'
import { NodeHttpHandler } from '@smithy/node-http-handler'
// 日志记录器
import { ManageLogger } from '../utils/logger' import { ManageLogger } from '../utils/logger'
// 端点地址格式化函数、错误格式化函数、获取请求代理、获取文件 MIME 类型、新的下载器、并发异步任务池
import { formatEndpoint, formatError, getAgent, getFileMimeType, NewDownloader, ConcurrencyPromisePool } from '../utils/common' import { formatEndpoint, formatError, getAgent, getFileMimeType, NewDownloader, ConcurrencyPromisePool } from '../utils/common'
// 是否为图片的判断函数、HTTP 代理格式化函数
import { isImage, formatHttpProxy } from '@/manage/utils/common' import { isImage, formatHttpProxy } from '@/manage/utils/common'
import { HttpsProxyAgent, HttpProxyAgent } from 'hpagent'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum' import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron' import { ipcMain, IpcMainEvent } from 'electron'
import UpDownTaskQueue,
{ // 上传下载任务队列
uploadTaskSpecialStatus, import UpDownTaskQueue, { uploadTaskSpecialStatus, commonTaskStatus } from '../datastore/upDownTaskQueue'
commonTaskStatus
} from '../datastore/upDownTaskQueue' // 文件系统库
import fs from 'fs-extra' import fs from 'fs-extra'
// 路径处理库
import path from 'path' import path from 'path'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static' import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
interface S3plistApiOptions { // dogecloudApi
credentials: { import { dogecloudApi, DogecloudToken, getTempToken } from '../utils/dogeAPI'
accessKeyId: string
secretAccessKey: string
}
endpoint?: string
sslEnabled: boolean
s3ForcePathStyle: boolean
httpOptions?: {
agent: https.Agent
}
}
class S3plistApi { class S3plistApi {
baseOptions: S3plistApiOptions baseOptions: S3ClientConfig
logger: ManageLogger logger: ManageLogger
agent: any agent: any
proxy: string | undefined proxy: string | undefined
dogeCloudSupport: boolean
accessKeyId: string
secretAccessKey: string
bucketName: string
constructor ( constructor (
accessKeyId: string, accessKeyId: string,
@@ -59,38 +76,66 @@ class S3plistApi {
sslEnabled: boolean, sslEnabled: boolean,
s3ForcePathStyle: boolean, s3ForcePathStyle: boolean,
proxy: string | undefined, proxy: string | undefined,
logger: ManageLogger logger: ManageLogger,
dogeCloudSupport: boolean = false,
bucketName: string = ''
) { ) {
this.accessKeyId = accessKeyId
this.secretAccessKey = secretAccessKey
this.dogeCloudSupport = dogeCloudSupport
this.bucketName = bucketName
this.baseOptions = { this.baseOptions = {
credentials: { credentials: {
accessKeyId, accessKeyId,
secretAccessKey secretAccessKey
}, },
endpoint: endpoint ? formatEndpoint(endpoint, sslEnabled) : undefined, endpoint: endpoint ? formatEndpoint(endpoint, sslEnabled) : undefined,
sslEnabled, tls: sslEnabled,
s3ForcePathStyle, forcePathStyle: s3ForcePathStyle,
httpOptions: { requestHandler: this.setAgent(proxy, sslEnabled)
agent: this.setAgent(proxy, sslEnabled) }
}
} as S3plistApiOptions
this.logger = logger this.logger = logger
this.agent = this.setAgent(proxy, sslEnabled)
this.proxy = formatHttpProxy(proxy, 'string') as string | undefined this.proxy = formatHttpProxy(proxy, 'string') as string | undefined
} }
setAgent (proxy: string | undefined, sslEnabled: boolean) : HttpProxyAgent | HttpsProxyAgent | undefined { async getDogeCloudToken () {
if (sslEnabled) { if (!this.dogeCloudSupport) return
const agent = getAgent(proxy, true).https const token = await getTempToken(this.accessKeyId, this.secretAccessKey) as DogecloudToken
return agent ?? new https.Agent({ if (Object.keys(token).length === 0) {
keepAlive: true, throw new Error('manage.setting.dogeCloudTokenError')
rejectUnauthorized: false
})
} else {
const agent = getAgent(proxy, false).http
return agent ?? new http.Agent({
keepAlive: true
})
} }
this.baseOptions.credentials = {
accessKeyId: token.accessKeyId,
secretAccessKey: token.secretAccessKey,
sessionToken: token.sessionToken
}
}
setAgent (proxy: string | undefined, sslEnabled: boolean) : NodeHttpHandler {
const agent = getAgent(proxy, sslEnabled)
const commonOptions: AgentOptions = {
keepAlive: true,
keepAliveMsecs: 1000,
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined
}
const extraOptions = sslEnabled ? { rejectUnauthorized: false } : {}
return sslEnabled
? new NodeHttpHandler({
httpsAgent: agent.https
? agent.https
: new https.Agent({
...commonOptions,
...extraOptions
})
})
: new NodeHttpHandler({
httpAgent: agent.http
? agent.http
: new http.Agent({
...commonOptions,
...extraOptions
})
})
} }
logParam = (error:any, method: string) => logParam = (error:any, method: string) =>
@@ -111,17 +156,18 @@ 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 { return {
...item, ...item,
key: item.Key, key: item.Key,
url: `${urlPrefix}/${item.Key}`, url: `${urlPrefix}/${item.Key}`,
fileName: item.Key?.replace(slicedPrefix, ''), fileName,
fileSize: item.Size, fileSize: item.Size,
formatedTime: new Date(item.LastModified!).toLocaleString(), formatedTime: new Date(item.LastModified!).toLocaleString(),
isDir: false, isDir: false,
checked: false, checked: false,
match: false, match: false,
isImage: isImage(item.Key?.replace(slicedPrefix, '') || '') isImage: isImage(fileName || '')
} }
} }
@@ -129,51 +175,64 @@ 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)
for (const item of res.buckets) {
if (item.name === this.bucketName || item.s3Bucket === this.bucketName) {
return [
{
Name: item.s3Bucket,
CreationDate: item.ctime,
Location: item.region
}
]
}
}
return []
} catch (error) {
this.logParam(error, 'getBucketList')
}
return []
}
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
const result = [] as IStringKeyMap[] const result: IStringKeyMap[] = []
const endpoint = options.endpoint as string || '' as string const endpoint = options.endpoint as string || ''
options.region = endpoint.indexOf('cloudflarestorage') !== -1 ? 'auto' : 'us-east-1' options.region = endpoint.includes('cloudflarestorage') ? 'auto' : 'us-east-1'
try { try {
const client = new S3Client(options) const client = new S3Client(options)
const command = new ListBucketsCommand({}) const data = await client.send(new ListBucketsCommand({}))
const data = await client.send(command)
if (data.$metadata.httpStatusCode === 200) { if (data.$metadata.httpStatusCode !== 200) {
if (data.Buckets) { this.logParam(data, 'getBucketList')
if (endpoint.indexOf('cloudflarestorage') !== -1) { return result
data.Buckets.forEach((bucket) => { }
result.push({
Name: bucket.Name, if (data.Buckets) {
CreationDate: bucket.CreationDate, if (endpoint.includes('cloudflarestorage')) {
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
}))
result.push({
Name: bucketName,
CreationDate: bucket.CreationDate,
Location: bucketConfig.$metadata.httpStatusCode === 200
? bucketConfig.LocationConstraint?.toLowerCase() || 'us-east-1'
: 'us-east-1'
}) })
} else { if (bucketConfig.$metadata.httpStatusCode !== 200) {
for (let i = 0; i < data.Buckets.length; i++) { this.logParam(bucketConfig, 'getBucketList')
const bucket = data.Buckets[i]
const bucketName = bucket.Name
const command = new GetBucketLocationCommand({
Bucket: bucketName
})
const bucketConfig = await client.send(command)
if (bucketConfig.$metadata.httpStatusCode === 200) {
result.push({
Name: bucketName,
CreationDate: bucket.CreationDate,
Location: bucketConfig.LocationConstraint?.toLowerCase() || 'us-east-1'
})
} else {
this.logParam(bucketConfig, 'getBucketList')
result.push({
Name: bucketName,
CreationDate: bucket.CreationDate,
Location: 'us-east-1'
})
}
} }
} }
} }
} else {
this.logParam(data, 'getBucketList')
} }
} catch (error) { } catch (error) {
this.logParam(error, 'getBucketList') this.logParam(error, 'getBucketList')
@@ -203,7 +262,7 @@ class S3plistApi {
try { try {
do { do {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: bucket, Bucket: bucket,
@@ -259,9 +318,10 @@ class S3plistApi {
finished: false finished: false
} }
try { try {
await this.getDogeCloudToken()
do { do {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: bucket, Bucket: bucket,
@@ -312,8 +372,8 @@ class S3plistApi {
success: false success: false
} }
try { try {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig await this.getDogeCloudToken()
options.region = region || 'us-east-1' const options = Object.assign({}, { ...this.baseOptions, region: String(region) || 'us-east-1' }) as S3ClientConfig
const client = new S3Client(options) const client = new S3Client(options)
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: bucket, Bucket: bucket,
@@ -324,12 +384,10 @@ class S3plistApi {
}) })
const data = await client.send(command) const data = await client.send(command)
if (data.$metadata.httpStatusCode === 200) { if (data.$metadata.httpStatusCode === 200) {
data.CommonPrefixes && data.CommonPrefixes.forEach((item: CommonPrefix) => { result.fullList = [
result.fullList.push(this.formatFolder(item, slicedPrefix)) ...(data.CommonPrefixes?.map(item => this.formatFolder(item, slicedPrefix)) || []),
}) ...(data.Contents?.map(item => this.formatFile(item, slicedPrefix, urlPrefix)) || [])
data.Contents && data.Contents.forEach((item: _Object) => { ]
result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
})
result.isTruncated = data.IsTruncated || false result.isTruncated = data.IsTruncated || false
result.nextMarker = data.NextContinuationToken || '' result.nextMarker = data.NextContinuationToken || ''
result.success = true result.success = true
@@ -354,8 +412,8 @@ class S3plistApi {
const { bucketName, region, oldKey, newKey } = configMap const { bucketName, region, oldKey, newKey } = configMap
let result = false let result = false
try { try {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig await this.getDogeCloudToken()
options.region = region || 'us-east-1' const options = Object.assign({}, { ...this.baseOptions, region: String(region) || 'us-east-1' }) as S3ClientConfig
const client = new S3Client(options) const client = new S3Client(options)
const command = new CopyObjectCommand({ const command = new CopyObjectCommand({
Bucket: bucketName, Bucket: bucketName,
@@ -396,8 +454,9 @@ class S3plistApi {
const { bucketName, region, key } = configMap const { bucketName, region, key } = configMap
let result = false let result = false
try { try {
await this.getDogeCloudToken()
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const command = new DeleteObjectCommand({ const command = new DeleteObjectCommand({
Bucket: bucketName, Bucket: bucketName,
@@ -430,9 +489,10 @@ class S3plistApi {
Contents: [] as any[] Contents: [] as any[]
} }
try { try {
await this.getDogeCloudToken()
do { do {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: bucketName, Bucket: bucketName,
@@ -467,7 +527,7 @@ class S3plistApi {
if (allFileList.Contents.length > 0) { if (allFileList.Contents.length > 0) {
const cycle = Math.ceil(allFileList.Contents.length / 1000) const cycle = Math.ceil(allFileList.Contents.length / 1000)
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
for (let i = 0; i < cycle; i++) { for (let i = 0; i < cycle; i++) {
const deleteList = allFileList.Contents.slice(i * 1000, (i + 1) * 1000) const deleteList = allFileList.Contents.slice(i * 1000, (i + 1) * 1000)
@@ -510,8 +570,9 @@ class S3plistApi {
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> { async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
const { bucketName, region, key, expires } = configMap const { bucketName, region, key, expires } = configMap
try { try {
await this.getDogeCloudToken()
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const signedUrl = await getSignedUrl(client, new GetObjectCommand({ const signedUrl = await getSignedUrl(client, new GetObjectCommand({
Bucket: bucketName, Bucket: bucketName,
@@ -534,8 +595,9 @@ class S3plistApi {
const { bucketName, region, key } = configMap const { bucketName, region, key } = configMap
let result = false let result = false
try { try {
await this.getDogeCloudToken()
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: bucketName, Bucket: bucketName,
@@ -573,14 +635,10 @@ class S3plistApi {
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) { for (const item of fileArray) {
const { bucketName, region, key, filePath, fileName, aclForUpload } = item const { bucketName, region, key, filePath, fileName, aclForUpload } = item
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const id = `${bucketName}-${String(region)}-${key}-${filePath}`
options.region = region || 'us-east-1'
const client = new S3Client(options)
const id = `${bucketName}-${region}-${key}-${filePath}`
if (instance.getUploadTask(id)) { if (instance.getUploadTask(id)) {
continue continue
} }
const fileStream = fs.createReadStream(filePath)
instance.addUploadTask({ instance.addUploadTask({
id, id,
progress: 0, progress: 0,
@@ -589,8 +647,25 @@ class S3plistApi {
sourceFilePath: filePath, sourceFilePath: filePath,
targetFilePath: key, targetFilePath: key,
targetFileBucket: bucketName, targetFileBucket: bucketName,
targetFileRegion: region targetFileRegion: String(region)
}) })
try {
await this.getDogeCloudToken()
} catch (error) {
this.logParam(error, 'uploadBucketFile')
instance.updateUploadTask({
id,
progress: 0,
status: commonTaskStatus.failed,
response: JSON.stringify(error),
finishTime: new Date().toLocaleString()
})
continue
}
const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = String(region) || 'us-east-1'
const client = new S3Client(options)
const fileStream = fs.createReadStream(filePath)
const parallelUploads3 = new Upload({ const parallelUploads3 = new Upload({
client, client,
params: { params: {
@@ -652,7 +727,7 @@ class S3plistApi {
for (const item of fileArray) { for (const item of fileArray) {
const { bucketName, region, key, fileName, customUrl } = item const { bucketName, region, key, fileName, customUrl } = item
const savedFilePath = path.join(downloadPath, fileName) const savedFilePath = path.join(downloadPath, fileName)
const id = `${bucketName}-${region}-${key}-${savedFilePath}` const id = `${bucketName}-${String(region)}-${key}-${savedFilePath}`
if (instance.getDownloadTask(id)) { if (instance.getDownloadTask(id)) {
continue continue
} }
@@ -665,7 +740,7 @@ class S3plistApi {
}) })
const preSignedUrl = await this.getPreSignedUrl({ const preSignedUrl = await this.getPreSignedUrl({
bucketName, bucketName,
region, region: String(region),
key, key,
expires: 36000, expires: 36000,
customUrl customUrl
+459
View File
@@ -0,0 +1,459 @@
// 日志记录器
import ManageLogger from '../utils/logger'
// SSH 客户端
import SSHClient from '~/main/utils/sshClient'
// 错误格式化函数、新的下载器、并发异步任务池
import { formatError } from '../utils/common'
// 是否为图片的判断函数
import { isImage } from '@/manage/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron'
// 上传下载任务队列
import UpDownTaskQueue, { commonTaskStatus, downloadTaskSpecialStatus, uploadTaskSpecialStatus } from '../datastore/upDownTaskQueue'
// 路径处理库
import path from 'path'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
import { Undefinable } from '~/universal/types/manage'
interface listDirResult {
permissions: string
isDir: boolean
owner: string
group: string
size: number
mtime: string
filename: string
key: string
}
class SftpApi {
host: string
port: number
username: string
password: string
privateKey: string
passphrase: string
fileMode: string
dirMode: string
logger: ManageLogger
ctx: SSHClient
config: {
host: string
port: number
username: string
password: string
privateKey: string
passphrase: string
}
constructor (
host: string,
port: Undefinable<number>,
username: Undefinable<string>,
password: Undefinable<string>,
privateKey: Undefinable<string>,
passphrase: Undefinable<string>,
fileMode: Undefinable<string>,
dirMode: Undefinable<string>,
logger: ManageLogger
) {
this.host = host
this.port = Number(port) || 22
this.username = username || ''
this.password = password || ''
this.privateKey = privateKey || ''
this.passphrase = passphrase || ''
this.fileMode = fileMode || '0664'
this.dirMode = dirMode || '0775'
this.logger = logger
this.ctx = SSHClient.instance
this.config = {
host: this.host,
port: this.port,
username: this.username,
password: this.password,
privateKey: this.privateKey,
passphrase: this.passphrase
}
}
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
let result = ''
for (let i = 0; i < 3; i++) {
const chunk = permissions.slice(i * 3, i * 3 + 3)
let value = 0
if (chunk[0] === 'r') value += 4
if (chunk[1] === 'w') value += 2
if (chunk[2] === 'x') value += 1
result += value
}
return `0${result}`
}
formatFolder (item: listDirResult, urlPrefix: string, isWebPath = false) {
const key = item.key
let url: string
if (isWebPath) {
url = urlPrefix
} else {
if (this.username && this.password) {
url = `sfpt://${this.username}:${this.password}@${urlPrefix}${item.filename}`
} else {
url = `${urlPrefix}${item.filename}`
}
}
return {
...item,
key,
fileName: item.filename,
fileSize: 0,
Key: key,
formatedTime: '',
isDir: true,
checked: false,
isImage: false,
match: false,
url
}
}
formatFile (item: listDirResult, urlPrefix: string, isWebPath = false) {
const key = item.key
return {
...item,
key,
fileName: item.filename,
fileSize: item.size,
Key: key,
formatedTime: new Date(item.mtime).toLocaleString(),
isDir: false,
checked: false,
match: false,
isImage: isImage(item.filename),
url: isWebPath ? urlPrefix : `${urlPrefix}${item.filename}`
}
}
isRequestSuccess = (code: number | null) => code === 0
connectClient = async () => {
try {
await this.ctx.connect(this.config)
if (!this.ctx.isConnected) {
throw new Error('SSH 未连接')
}
} catch (error) {
this.logParam(error, 'connectClient')
}
}
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}`
const cancelTask = [false]
ipcMain.on(cancelDownloadLoadingFileList, (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) {
cancelTask[0] = true
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
}
})
let res = {} as any
const result = {
fullList: <any>[],
success: false,
finished: false
}
try {
await this.connectClient()
res = await this.ctx.execCommand(`cd "${prefix}" && ls -la --time-style=long-iso`)
this.ctx.close()
if (this.isRequestSuccess(res.code)) {
const formatedLSRes = this.formatLSResult(res.stdout, prefix)
if (formatedLSRes.length) {
formatedLSRes.forEach((item: listDirResult) => {
if (!item.isDir) {
result.fullList.push(this.formatFile(item, urlPrefix))
}
})
}
result.success = true
}
} catch (error) {
this.logParam(error, 'getBucketListRecursively')
}
result.finished = true
window.webContents.send(refreshDownloadFileTransferList, result)
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
}
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) => {
const [permissions, , owner, group, size, date, time, ...name] = item.trim().split(/\s+/)
const filename = name.join(' ')
if (filename === '.' || filename === '..') {
return
}
const isDir = permissions.startsWith('d')
const mtime = `${date} ${time}`
const key = path.join(cwd, filename).replace(/\\/g, '/').replace(/^\/+/, '')
result.push({
permissions,
isDir,
owner,
group,
size: Number(size) || 0,
mtime,
filename,
key
})
})
return result
}
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}`
urlPrefix = urlPrefix.replace(/\/+$/, '')
let webPath = configMap.webPath || ''
if (webPath && customUrl && webPath !== '/') {
webPath = webPath.replace(/^\/+|\/+$/, '')
}
const cancelTask = [false]
ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) {
cancelTask[0] = true
ipcMain.removeAllListeners('cancelLoadingFileList')
}
})
let res = {} as any
const result = {
fullList: <any>[],
success: false,
finished: false
}
try {
await this.connectClient()
res = await this.ctx.execCommand(`cd "${prefix}" && ls -la --time-style=long-iso`)
this.ctx.close()
if (this.isRequestSuccess(res.code)) {
const formatedLSRes = this.formatLSResult(res.stdout, prefix)
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, '/')
if (item.isDir) {
result.fullList.push(this.formatFolder(item, webPath ? relative : urlPrefix, !!webPath))
} else {
result.fullList.push(this.formatFile(item, webPath ? relative : urlPrefix, !!webPath))
}
})
}
} else {
result.finished = true
window.webContents.send('refreshFileTransferList', result)
ipcMain.removeAllListeners('cancelLoadingFileList')
return
}
} catch (error) {
this.logParam(error, 'getBucketListBackstage')
result.finished = true
window.webContents.send('refreshFileTransferList', result)
ipcMain.removeAllListeners('cancelLoadingFileList')
return
}
result.success = true
result.finished = true
window.webContents.send('refreshFileTransferList', result)
ipcMain.removeAllListeners('cancelLoadingFileList')
}
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { oldKey, newKey } = configMap
let result = false
try {
await this.connectClient()
const res = await this.ctx.execCommand(`mv -f "/${oldKey.replace(/^\/+/, '')}" "/${newKey.replace(/^\/+/, '')}"`)
this.ctx.close()
result = this.isRequestSuccess(res.code)
} catch (error) {
this.logParam(error, 'renameBucketFile')
}
return result
}
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { key } = configMap
let result = false
try {
await this.connectClient()
const res = await this.ctx.execCommand(`rm -f "/${key.replace(/^\/+/, '')}"`)
this.ctx.close()
result = this.isRequestSuccess(res.code)
} catch (error) {
this.logParam(error, 'deleteBucketFile')
}
return result
}
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { key } = configMap
let result = false
try {
await this.connectClient()
if (key.replace(/^\/+/, '') === '' || key.includes('*')) {
throw new Error('禁止删除')
}
const res = await this.ctx.execCommand(`rm -rf "/${key.replace(/^\/+/, '')}"`)
this.ctx.close()
result = this.isRequestSuccess(res.code)
} catch (error) {
this.logParam(error, 'deleteBucketFolder')
}
return result
}
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { fileArray } = configMap
const instance = UpDownTaskQueue.getInstance()
for (const item of fileArray) {
const { alias, bucketName, region, key, filePath, fileName } = item
const id = `${alias}-${bucketName}-${key}-${filePath}`
if (instance.getUploadTask(id)) {
continue
}
instance.addUploadTask({
id,
progress: 0,
status: commonTaskStatus.queuing,
sourceFileName: fileName,
sourceFilePath: filePath,
targetFilePath: key,
targetFileBucket: bucketName,
targetFileRegion: region,
noProgress: false
})
try {
await this.connectClient()
const res = await this.ctx.putFile(filePath, `/${key.replace(/^\/+/, '')}`, {
fileMode: this.fileMode,
dirMode: this.dirMode
})
this.ctx.close()
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) {
this.logParam(error, 'uploadBucketFile')
instance.updateUploadTask({
id,
progress: 0,
status: commonTaskStatus.failed,
finishTime: new Date().toLocaleString()
})
}
}
return true
}
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { key } = configMap
let result = false
try {
await this.connectClient()
const res = await this.ctx.execCommand(`mkdir -p "/${key.replace(/^\/+/, '')}"`)
this.ctx.close()
result = this.isRequestSuccess(res.code)
} catch (error) {
this.logParam(error, 'createBucketFolder')
}
return result
}
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { downloadPath, fileArray } = configMap
const instance = UpDownTaskQueue.getInstance()
for (const item of fileArray) {
const { alias, bucketName, region, key, fileName } = item
const savedFilePath = path.join(downloadPath, fileName)
const id = `${alias}-${bucketName}-${region}-${key}`
if (instance.getDownloadTask(id)) {
continue
}
instance.addDownloadTask({
id,
progress: 0,
status: commonTaskStatus.queuing,
sourceFileName: fileName,
targetFilePath: savedFilePath
})
try {
await this.connectClient()
const res = await this.ctx.getFile(savedFilePath, `/${key.replace(/^\/+/, '')}`)
this.ctx.close()
if (res) {
instance.updateDownloadTask({
id,
progress: 100,
status: downloadTaskSpecialStatus.downloaded,
finishTime: new Date().toLocaleString()
})
} else {
instance.updateDownloadTask({
id,
progress: 0,
status: commonTaskStatus.failed,
finishTime: new Date().toLocaleString()
})
}
} catch (error) {
this.logParam(error, 'downloadBucketFile')
instance.updateDownloadTask({
id,
progress: 0,
status: commonTaskStatus.failed,
finishTime: new Date().toLocaleString()
})
}
}
return true
}
}
export default SftpApi
+41 -28
View File
@@ -1,13 +1,34 @@
// 是否为图片的判断函数
import { isImage } from '@/manage/utils/common' import { isImage } from '@/manage/utils/common'
// Axios 和 Axios 实例类型声明
import axios, { AxiosInstance } from 'axios' import axios, { AxiosInstance } from 'axios'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum' import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron' import { ipcMain, IpcMainEvent } from 'electron'
// 表单数据库
import FormData from 'form-data' import FormData from 'form-data'
// 文件系统库
import fs from 'fs-extra' import fs from 'fs-extra'
// 获取文件 MIME 类型、got 上传函数、新的下载器、并发异步任务池、错误格式化函数
import { getFileMimeType, gotUpload, NewDownloader, ConcurrencyPromisePool, formatError } from '../utils/common' import { getFileMimeType, gotUpload, NewDownloader, ConcurrencyPromisePool, formatError } from '../utils/common'
// 路径处理库
import path from 'path' import path from 'path'
// 上传下载任务队列
import UpDownTaskQueue, { commonTaskStatus } from '../datastore/upDownTaskQueue' import UpDownTaskQueue, { commonTaskStatus } from '../datastore/upDownTaskQueue'
// 日志记录器
import { ManageLogger } from '../utils/logger' import { ManageLogger } from '../utils/logger'
class SmmsApi { class SmmsApi {
@@ -99,7 +120,7 @@ class SmmsApi {
return return
} }
marker++ marker++
} while (!cancelTask[0] && res && res.status === 200 && res.data && res.data.success && res.data.CurrentPage < res.data.TotalPages) } while (!cancelTask[0] && res?.status === 200 && res?.data?.success && res.data.CurrentPage < res.data.TotalPages)
result.success = !cancelTask[0] result.success = !cancelTask[0]
result.finished = true result.finished = true
window.webContents.send('refreshFileTransferList', result) window.webContents.send('refreshFileTransferList', result)
@@ -121,16 +142,14 @@ class SmmsApi {
* customUrl: string * customUrl: string
* } * }
*/ */
async getBucketFileList (configMap: IStringKeyMap): Promise<any> { async getBucketFileList ({ currentPage }: IStringKeyMap): Promise<any> {
const { currentPage } = configMap
let res = {} as any
const result = { const result = {
fullList: <any>[], fullList: <any>[],
isTruncated: false, isTruncated: false,
nextMarker: '', nextMarker: '',
success: false success: false
} }
res = await this.axiosInstance( const res = await this.axiosInstance(
'/upload_history', '/upload_history',
{ {
method: 'GET', method: 'GET',
@@ -142,21 +161,17 @@ class SmmsApi {
} }
} }
) )
if (res && res.status === 200 && res.data && res.data.success) { if (res?.status !== 200 || !res?.data?.success) return result
if (res.data.Count === 0) {
result.success = true if (res.data.Count === 0) return { ...result, success: true }
return result
} res.data.data.forEach((item: any) => {
res.data.data.forEach((item: any) => { result.fullList.push(this.formatFile(item))
result.fullList.push(this.formatFile(item)) })
}) result.isTruncated = res.data.CurrentPage < res.data.TotalPages
result.isTruncated = res.data.CurrentPage < res.data.TotalPages result.nextMarker = res.data.CurrentPage + 1
result.nextMarker = res.data.CurrentPage + 1 result.success = true
result.success = true return result
return result
} else {
return result
}
} }
/** /**
@@ -169,20 +184,18 @@ class SmmsApi {
* DeleteHash: string * DeleteHash: string
* } * }
*/ */
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> { async deleteBucketFile ({ DeleteHash }: IStringKeyMap): Promise<boolean> {
const { DeleteHash } = configMap
const params = {
hash: DeleteHash,
format: 'json'
}
const res = await this.axiosInstance( const res = await this.axiosInstance(
`/delete/${DeleteHash}`, `/delete/${DeleteHash}`,
{ {
method: 'GET', method: 'GET',
params params: {
hash: DeleteHash,
format: 'json'
}
} }
) )
return res && res.status === 200 && res.data && res.data.success return res?.status === 200 && res?.data?.success
} }
/** /**
+107 -141
View File
@@ -1,19 +1,37 @@
// 腾讯云 COS SDK
import COS from 'cos-nodejs-sdk-v5' import COS from 'cos-nodejs-sdk-v5'
// 文件系统库
import fs from 'fs-extra' import fs from 'fs-extra'
// 路径处理库
import path from 'path' import path from 'path'
// 是否为图片的判断函数
import { isImage } from '~/renderer/manage/utils/common' import { isImage } from '~/renderer/manage/utils/common'
// URL 编码处理函数
import { handleUrlEncode } from '~/universal/utils/common' import { handleUrlEncode } from '~/universal/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum' import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron' import { ipcMain, IpcMainEvent } from 'electron'
// 错误格式化函数、获取文件 MIME 类型
import { formatError, getFileMimeType } from '../utils/common' import { formatError, getFileMimeType } from '../utils/common'
import UpDownTaskQueue,
{ // 上传下载任务队列
uploadTaskSpecialStatus, import UpDownTaskQueue, { uploadTaskSpecialStatus, commonTaskStatus, downloadTaskSpecialStatus } from '../datastore/upDownTaskQueue'
commonTaskStatus,
downloadTaskSpecialStatus // 日志记录器
} from '../datastore/upDownTaskQueue'
import { ManageLogger } from '../utils/logger' import { ManageLogger } from '../utils/logger'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static' import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class TcyunApi { class TcyunApi {
@@ -62,7 +80,7 @@ class TcyunApi {
*/ */
async getBucketList (): Promise<any> { async getBucketList (): Promise<any> {
const res = await this.ctx.getService({}) const res = await this.ctx.getService({})
return res && res.Buckets ? res.Buckets : [] return res?.Buckets || []
} }
/** /**
@@ -74,21 +92,8 @@ class TcyunApi {
Bucket: bucketName, Bucket: bucketName,
Region: region Region: region
}) })
const result = [] as string[] if (res?.statusCode !== 200 || !res?.DomainRule?.length) return []
if (res && res.statusCode === 200) { return res.DomainRule.filter((item: any) => item.Status === 'ENABLED').map(item => item.Name)
if (res.DomainRule && res.DomainRule.length > 0) {
res.DomainRule.forEach((item: any) => {
if (item.Status === 'ENABLED') {
result.push(item.Name)
}
})
return result
} else {
return []
}
} else {
return []
}
} }
/** /**
@@ -113,31 +118,29 @@ class TcyunApi {
Bucket: configMap.BucketName, Bucket: configMap.BucketName,
Region: configMap.region Region: configMap.region
}) })
return res && res.statusCode === 200 return res?.statusCode === 200
} }
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> { async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)! const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const bucket = configMap.bucketName const { bucketName: bucket, bucketConfig: { Location: region }, prefix, customUrl, cancelToken } = configMap
const region = configMap.bucketConfig.Location
const prefix = configMap.prefix as string
const slicedPrefix = prefix.slice(1, prefix.length) const slicedPrefix = prefix.slice(1, prefix.length)
const urlPrefix = configMap.customUrl || `https://${bucket}.cos.${region}.myqcloud.com` const urlPrefix = customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
let marker
const cancelToken = configMap.cancelToken as string
const cancelTask = [false] const cancelTask = [false]
let marker
ipcMain.on(cancelDownloadLoadingFileList, (_evt: IpcMainEvent, token: string) => { ipcMain.on(cancelDownloadLoadingFileList, (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) { if (token === cancelToken) {
cancelTask[0] = true cancelTask[0] = true
ipcMain.removeAllListeners(cancelDownloadLoadingFileList) ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
} }
}) })
let res = {} as COS.GetBucketResult
const result = { const result = {
fullList: <any>[], fullList: <any>[],
success: false, success: false,
finished: false finished: false
} }
let res = {} as COS.GetBucketResult
do { do {
res = await this.ctx.getBucket({ res = await this.ctx.getBucket({
Bucket: bucket, Bucket: bucket,
@@ -145,9 +148,9 @@ class TcyunApi {
Prefix: slicedPrefix === '' ? undefined : slicedPrefix, Prefix: slicedPrefix === '' ? undefined : slicedPrefix,
Marker: marker Marker: marker
}) })
if (res && res.statusCode === 200) { if (res?.statusCode === 200) {
res.Contents.forEach((item: COS.CosObject) => result.fullList.push(...res.Contents.filter(item => parseInt(item.Size) !== 0)
parseInt(item.Size) !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))) .map(item => this.formatFile(item, slicedPrefix, urlPrefix)))
window.webContents.send(refreshDownloadFileTransferList, result) window.webContents.send(refreshDownloadFileTransferList, result)
} else { } else {
result.finished = true result.finished = true
@@ -165,14 +168,12 @@ class TcyunApi {
async getBucketListBackstage (configMap: IStringKeyMap): Promise < any > { async getBucketListBackstage (configMap: IStringKeyMap): Promise < any > {
const window = windowManager.get(IWindowList.SETTING_WINDOW)! const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const bucket = configMap.bucketName const { bucketName: bucket, bucketConfig: { Location: region }, prefix, customUrl, cancelToken } = configMap
const region = configMap.bucketConfig.Location
const prefix = configMap.prefix as string
const slicedPrefix = prefix.slice(1, prefix.length) const slicedPrefix = prefix.slice(1, prefix.length)
const urlPrefix = configMap.customUrl || `https://${bucket}.cos.${region}.myqcloud.com` const urlPrefix = customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
let marker
const cancelToken = configMap.cancelToken as string
const cancelTask = [false] const cancelTask = [false]
let marker
ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) { if (token === cancelToken) {
cancelTask[0] = true cancelTask[0] = true
@@ -193,11 +194,12 @@ class TcyunApi {
Delimiter: '/', Delimiter: '/',
Marker: marker Marker: marker
}) })
if (res && res.statusCode === 200) { if (res?.statusCode === 200) {
res.CommonPrefixes.forEach((item: { Prefix: string}) => result.fullList.push(
result.fullList.push(this.formatFolder(item, slicedPrefix))) ...res.CommonPrefixes.map(item => this.formatFolder(item, slicedPrefix)),
res.Contents.forEach((item: COS.CosObject) => ...res.Contents.filter(item => parseInt(item.Size) !== 0)
parseInt(item.Size) !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))) .map(item => this.formatFile(item, slicedPrefix, urlPrefix))
)
window.webContents.send('refreshFileTransferList', result) window.webContents.send('refreshFileTransferList', result)
} else { } else {
result.finished = true result.finished = true
@@ -229,36 +231,34 @@ class TcyunApi {
* } * }
*/ */
async getBucketFileList (configMap: IStringKeyMap): Promise<any> { async getBucketFileList (configMap: IStringKeyMap): Promise<any> {
const bucket = configMap.bucketName const { bucketName: bucket, bucketConfig: { Location: region }, prefix, customUrl, marker, itemsPerPage } = configMap
const region = configMap.bucketConfig.Location
const prefix = configMap.prefix as string
const slicedPrefix = prefix.slice(1) const slicedPrefix = prefix.slice(1)
const urlPrefix = configMap.customUrl || `https://${bucket}.cos.${region}.myqcloud.com` const urlPrefix = customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
const marker = configMap.marker as string const res = await this.ctx.getBucket({
const itemsPerPage = configMap.itemsPerPage as number
let res = {} as COS.GetBucketResult
const result = {
fullList: <any>[],
isTruncated: false,
nextMarker: '',
success: false
}
res = await this.ctx.getBucket({
Bucket: bucket, Bucket: bucket,
Region: region, Region: region,
Prefix: slicedPrefix === '' ? undefined : slicedPrefix, Prefix: slicedPrefix === '' ? undefined : slicedPrefix,
Delimiter: '/', Delimiter: '/',
Marker: marker, Marker: marker,
MaxKeys: itemsPerPage MaxKeys: itemsPerPage
}) }) as COS.GetBucketResult
if (res && res.statusCode === 200) { if (res?.statusCode !== 200) {
res.CommonPrefixes.forEach((item: { Prefix: string}) => return {
result.fullList.push(this.formatFolder(item, slicedPrefix))) fullList: [],
res.Contents.forEach((item: COS.CosObject) => isTruncated: false,
parseInt(item.Size) !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))) nextMarker: '',
result.isTruncated = res.IsTruncated === 'true' success: false
result.nextMarker = res.NextMarker || '' }
result.success = true }
const result = {
fullList: [
...res.CommonPrefixes.map(item => this.formatFolder(item, slicedPrefix)),
...res.Contents.filter(item => parseInt(item.Size) !== 0)
.map(item => this.formatFile(item, slicedPrefix, urlPrefix))
],
isTruncated: res.IsTruncated === 'true',
nextMarker: res.NextMarker || '',
success: true
} }
return result return result
} }
@@ -275,22 +275,22 @@ class TcyunApi {
*/ */
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> { async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName, region, oldKey, newKey } = configMap const { bucketName, region, oldKey, newKey } = configMap
const res = await this.ctx.putObjectCopy({ const copyRes = await this.ctx.putObjectCopy({
Bucket: bucketName, Bucket: bucketName,
Region: region, Region: region,
Key: newKey, Key: newKey,
CopySource: handleUrlEncode(`${bucketName}.cos.${region}.myqcloud.com/${oldKey}`) CopySource: handleUrlEncode(`${bucketName}.cos.${region}.myqcloud.com/${oldKey}`)
}) })
if (res && res.statusCode === 200) {
const res2 = await this.ctx.deleteObject({ if (copyRes?.statusCode !== 200) return false
Bucket: bucketName,
Region: region, const deleteRes = await this.ctx.deleteObject({
Key: oldKey Bucket: bucketName,
}) Region: region,
return res2 && res2.statusCode === 204 Key: oldKey
} else { })
return false
} return deleteRes?.statusCode === 204
} }
/** /**
@@ -309,7 +309,7 @@ class TcyunApi {
Region: region, Region: region,
Key: key Key: key
}) })
return res && res.statusCode === 204 return res?.statusCode === 204
} }
/** /**
@@ -319,72 +319,38 @@ class TcyunApi {
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> { async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName, region, key } = configMap const { bucketName, region, key } = configMap
let marker let marker
let isTruncated let res: any
const allFileList = { const allFileList = {
CommonPrefixes: [] as any[], CommonPrefixes: [] as any[],
Contents: [] as any[] Contents: [] as any[]
} }
let res = await this.ctx.getBucket({ do {
Bucket: bucketName, res = await this.ctx.getBucket({
Region: region, Bucket: bucketName,
Prefix: key, Region: region,
Delimiter: '/', Prefix: key,
MaxKeys: 1000 Delimiter: '/',
}) MaxKeys: 1000,
if (res && res.statusCode === 200) { Marker: marker
res.CommonPrefixes.length > 0 && allFileList.CommonPrefixes.push(...res.CommonPrefixes) })
res.Contents.length > 0 && allFileList.Contents.push(...res.Contents)
isTruncated = res.IsTruncated if (res?.statusCode !== 200) return false
allFileList.CommonPrefixes.push(...res.CommonPrefixes)
allFileList.Contents.push(...res.Contents)
marker = res.NextMarker marker = res.NextMarker
while (isTruncated === 'true') { } while (res.IsTruncated === 'true')
res = await this.ctx.getBucket({ for (const item of allFileList.CommonPrefixes) {
Bucket: bucketName, if (!(await this.deleteBucketFolder({ bucketName, region, key: item.Prefix }))) return false
Region: region,
Prefix: key,
Delimiter: '/',
Marker: marker,
MaxKeys: 1000
}) as any
if (res && res.statusCode === 200) {
res.CommonPrefixes.length > 0 && allFileList.CommonPrefixes.push(...res.CommonPrefixes)
res.Contents.length > 0 && allFileList.Contents.push(...res.Contents)
isTruncated = res.IsTruncated
marker = res.NextMarker
} else {
return false
}
}
} else {
return false
} }
if (allFileList.CommonPrefixes.length > 0) { const cycles = Math.ceil(allFileList.Contents.length / 1000)
for (const item of allFileList.CommonPrefixes) { for (let i = 0; i < cycles; i++) {
res = await this.deleteBucketFolder({ const res = await this.ctx.deleteMultipleObject({
bucketName, Bucket: bucketName,
region, Region: region,
key: item.Prefix Objects: allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => ({ Key: item.Key }))
}) as any })
if (!res) { if (res?.statusCode !== 200) return false
return false
}
}
}
if (allFileList.Contents.length > 0) {
const cycle = Math.ceil(allFileList.Contents.length / 1000)
for (let i = 0; i < cycle; i++) {
res = await this.ctx.deleteMultipleObject({
Bucket: bucketName,
Region: region,
Objects: allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => {
return {
Key: item.Key
}
})
}) as any
if (!(res && res.statusCode === 200)) {
return false
}
}
} }
return true return true
} }
@@ -410,7 +376,7 @@ class TcyunApi {
Sign: true Sign: true
}, () => { }, () => {
}) })
return customUrl ? `${customUrl.replace(/\/$/, '')}/${key}${res.slice(res.indexOf('?'))}` : res return customUrl ? `${customUrl.replace(/\/+$/, '')}/${key}${res.slice(res.indexOf('?'))}` : res
} }
/** /**
@@ -500,7 +466,7 @@ class TcyunApi {
Key: key, Key: key,
Body: '' Body: ''
}) })
return res && res.statusCode === 200 return res?.statusCode === 200
} }
/** /**
+39 -23
View File
@@ -1,19 +1,41 @@
// 忽略 TypeScript 错误
// @ts-ignore // @ts-ignore
import Upyun from 'upyun' import Upyun from 'upyun'
// 加密函数、获取文件 MIME 类型、新的下载器、got 上传函数、并发异步任务池、错误格式化函数
import { md5, hmacSha1Base64, getFileMimeType, NewDownloader, gotUpload, ConcurrencyPromisePool, formatError } from '../utils/common' import { md5, hmacSha1Base64, getFileMimeType, NewDownloader, gotUpload, ConcurrencyPromisePool, formatError } from '../utils/common'
// 是否为图片的判断函数
import { isImage } from '~/renderer/manage/utils/common' import { isImage } from '~/renderer/manage/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum' import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron' import { ipcMain, IpcMainEvent } from 'electron'
// Axios
import axios from 'axios' import axios from 'axios'
// 表单数据库
import FormData from 'form-data' import FormData from 'form-data'
// 文件系统库
import fs from 'fs-extra' import fs from 'fs-extra'
// 路径处理库
import path from 'path' import path from 'path'
import UpDownTaskQueue,
{ // 上传下载任务队列
commonTaskStatus import UpDownTaskQueue, { commonTaskStatus } from '../datastore/upDownTaskQueue'
} from '../datastore/upDownTaskQueue'
// 日志记录器
import { ManageLogger } from '../utils/logger' import { ManageLogger } from '../utils/logger'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static' import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class UpyunApi { class UpyunApi {
@@ -35,9 +57,10 @@ class UpyunApi {
} }
formatFolder (item: any, slicedPrefix: string) { formatFolder (item: any, slicedPrefix: string) {
const key = `${slicedPrefix}${item.name}/`
return { return {
...item, ...item,
key: `${slicedPrefix}${item.name}/`, key,
fileSize: 0, fileSize: 0,
formatedTime: '', formatedTime: '',
fileName: item.name, fileName: item.name,
@@ -45,11 +68,12 @@ class UpyunApi {
checked: false, checked: false,
isImage: false, isImage: false,
match: false, match: false,
Key: `${slicedPrefix}${item.name}/` Key: key
} }
} }
formatFile (item: any, slicedPrefix: string, urlPrefix: string) { formatFile (item: any, slicedPrefix: string, urlPrefix: string) {
const key = `${slicedPrefix}${item.name}`
return { return {
...item, ...item,
fileName: item.name, fileName: item.name,
@@ -59,8 +83,8 @@ class UpyunApi {
checked: false, checked: false,
match: false, match: false,
isImage: isImage(item.name), isImage: isImage(item.name),
url: `${urlPrefix}/${slicedPrefix}${item.name}`, url: `${urlPrefix}/${key}`,
key: `${slicedPrefix}${item.name}` key
} }
} }
@@ -71,18 +95,10 @@ class UpyunApi {
operator: string, operator: string,
password: string password: string
) { ) {
const passwordMd5 = md5(password, 'hex') return `UPYUN ${operator}:${hmacSha1Base64(
const date = new Date().toUTCString() md5(password, 'hex'),
const upperMethod = method.toUpperCase() `${method.toUpperCase()}&${encodeURI(uri)}&${new Date().toUTCString()}${contentMd5 ? `&${contentMd5}` : ''}`
let stringToSign = '' )}`
const codedUri = encodeURI(uri)
if (contentMd5 === '') {
stringToSign = `${upperMethod}&${codedUri}&${date}`
} else {
stringToSign = `${upperMethod}&${codedUri}&${date}&${contentMd5}`
}
const signature = hmacSha1Base64(passwordMd5, stringToSign)
return `UPYUN ${operator}:${signature}`
} }
/** /**
@@ -120,7 +136,7 @@ class UpyunApi {
iter: marker iter: marker
}) })
if (res) { if (res) {
res.files && res.files.forEach((item: any) => { res.files?.forEach((item: any) => {
item.type === 'F' && folderQueue.push(`${slicedPrefix}${item.name}/`) item.type === 'F' && folderQueue.push(`${slicedPrefix}${item.name}/`)
item.type === 'N' && result.fullList.push(this.formatFile(item, folder, urlPrefix)) item.type === 'N' && result.fullList.push(this.formatFile(item, folder, urlPrefix))
}) })
@@ -169,7 +185,7 @@ class UpyunApi {
iter: marker iter: marker
}) })
if (res) { if (res) {
res.files && res.files.forEach((item: any) => { res.files?.forEach((item: any) => {
item.type === 'N' && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) item.type === 'N' && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
item.type === 'F' && result.fullList.push(this.formatFolder(item, slicedPrefix)) item.type === 'F' && result.fullList.push(this.formatFolder(item, slicedPrefix))
}) })
@@ -219,7 +235,7 @@ class UpyunApi {
iter: marker || '' iter: marker || ''
}) })
if (res) { if (res) {
res.files && res.files.forEach((item: any) => { res.files?.forEach((item: any) => {
item.type === 'N' && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) item.type === 'N' && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
item.type === 'F' && result.fullList.push(this.formatFolder(item, slicedPrefix)) item.type === 'F' && result.fullList.push(this.formatFolder(item, slicedPrefix))
}) })
+48 -29
View File
@@ -1,19 +1,38 @@
// 日志记录器
import ManageLogger from '../utils/logger' import ManageLogger from '../utils/logger'
// WebDAV 客户端库
import { createClient, WebDAVClient, FileStat, ProgressEvent } from 'webdav' import { createClient, WebDAVClient, FileStat, ProgressEvent } from 'webdav'
// 错误格式化函数、端点地址格式化函数、获取内部代理、新的下载器、并发异步任务池
import { formatError, formatEndpoint, getInnerAgent, NewDownloader, ConcurrencyPromisePool } from '../utils/common' import { formatError, formatEndpoint, getInnerAgent, NewDownloader, ConcurrencyPromisePool } from '../utils/common'
// HTTP 代理格式化函数、是否为图片的判断函数
import { formatHttpProxy, isImage } from '@/manage/utils/common' import { formatHttpProxy, isImage } from '@/manage/utils/common'
// HTTP 和 HTTPS 模块
import http from 'http' import http from 'http'
import https from 'https' import https from 'https'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager' import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum' import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron' import { ipcMain, IpcMainEvent } from 'electron'
import UpDownTaskQueue,
{ // 上传下载任务队列
uploadTaskSpecialStatus, import UpDownTaskQueue, { uploadTaskSpecialStatus, commonTaskStatus } from '../datastore/upDownTaskQueue'
commonTaskStatus
} from '../datastore/upDownTaskQueue' // 文件系统库
import fs from 'fs-extra' import fs from 'fs-extra'
// 路径处理库
import path from 'path' import path from 'path'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static' import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class WebdavplistApi { class WebdavplistApi {
@@ -52,35 +71,37 @@ class WebdavplistApi {
logParam = (error:any, method: string) => logParam = (error:any, method: string) =>
this.logger.error(formatError(error, { class: 'WebdavplistApi', method })) this.logger.error(formatError(error, { class: 'WebdavplistApi', method }))
formatFolder (item: FileStat, urlPrefix: string) { formatFolder (item: FileStat, urlPrefix: string, isWebPath = false) {
const key = item.filename.replace(/^\/+/, '')
return { return {
...item, ...item,
key: item.filename.replace(/^\/+/, ''), key,
fileName: item.basename, fileName: item.basename,
fileSize: 0, fileSize: 0,
Key: item.filename.replace(/^\/+/, ''), Key: key,
formatedTime: '', formatedTime: '',
isDir: true, isDir: true,
checked: false, checked: false,
isImage: false, isImage: false,
match: false, match: false,
url: `${urlPrefix}${item.filename}` url: isWebPath ? urlPrefix : `${urlPrefix}${item.filename}`
} }
} }
formatFile (item: FileStat, urlPrefix: string) { formatFile (item: FileStat, urlPrefix: string, isWebPath = false) {
const key = item.filename.replace(/^\/+/, '')
return { return {
...item, ...item,
key: item.filename.replace(/^\/+/, ''), key,
fileName: item.basename, fileName: item.basename,
fileSize: item.size, fileSize: item.size,
Key: item.filename.replace(/^\/+/, ''), Key: key,
formatedTime: new Date(item.lastmod).toLocaleString(), formatedTime: new Date(item.lastmod).toLocaleString(),
isDir: false, isDir: false,
checked: false, checked: false,
match: false, match: false,
isImage: isImage(item.basename), isImage: isImage(item.basename),
url: `${urlPrefix}${item.filename}` url: isWebPath ? urlPrefix : `${urlPrefix}${item.filename}`
} }
} }
@@ -109,27 +130,18 @@ class WebdavplistApi {
details: true details: true
}) })
if (this.isRequestSuccess(res.status)) { if (this.isRequestSuccess(res.status)) {
if (res.data && res.data.length) { if (res.data?.length) {
res.data.forEach((item: FileStat) => { res.data.forEach((item: FileStat) => {
if (item.type !== 'directory') { if (item.type !== 'directory') {
result.fullList.push(this.formatFile(item, urlPrefix)) result.fullList.push(this.formatFile(item, urlPrefix))
} }
}) })
} }
} else { result.success = true
result.finished = true
window.webContents.send(refreshDownloadFileTransferList, result)
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
return
} }
} catch (error) { } catch (error) {
this.logParam(error, 'getBucketListRecursively') this.logParam(error, 'getBucketListRecursively')
result.finished = true
window.webContents.send(refreshDownloadFileTransferList, result)
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
return
} }
result.success = true
result.finished = true result.finished = true
window.webContents.send(refreshDownloadFileTransferList, result) window.webContents.send(refreshDownloadFileTransferList, result)
ipcMain.removeAllListeners(cancelDownloadLoadingFileList) ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
@@ -137,8 +149,13 @@ class WebdavplistApi {
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> { async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)! const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const { prefix, customUrl, cancelToken } = configMap const { prefix, customUrl, cancelToken, baseDir } = configMap
const urlPrefix = customUrl || this.endpoint let urlPrefix = customUrl || this.endpoint
urlPrefix = urlPrefix.replace(/\/+$/, '')
let webPath = configMap.webPath || ''
if (webPath && customUrl && webPath !== '/') {
webPath = webPath.replace(/^\/+|\/+$/, '')
}
const cancelTask = [false] const cancelTask = [false]
ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) { if (token === cancelToken) {
@@ -158,12 +175,14 @@ class WebdavplistApi {
details: true details: true
}) })
if (this.isRequestSuccess(res.status)) { if (this.isRequestSuccess(res.status)) {
if (res.data && res.data.length) { if (res.data?.length) {
res.data.forEach((item: FileStat) => { res.data.forEach((item: FileStat) => {
const relativePath = path.relative(baseDir, item.filename)
const relative = webPath && urlPrefix + `/${path.join(webPath, relativePath)}`.replace(/\\/g, '/').replace(/\/+/g, '/')
if (item.type === 'directory') { if (item.type === 'directory') {
result.fullList.push(this.formatFolder(item, urlPrefix)) result.fullList.push(this.formatFolder(item, webPath ? relative : urlPrefix, !!webPath))
} else { } else {
result.fullList.push(this.formatFile(item, urlPrefix)) result.fullList.push(this.formatFile(item, webPath ? relative : urlPrefix, !!webPath))
} }
}) })
} }
+35 -18
View File
@@ -54,22 +54,26 @@ export class ManageApi extends EventEmitter implements ManageApiType {
createClient () { createClient () {
const name = this.currentPicBedConfig.picBedName const name = this.currentPicBedConfig.picBedName
switch (name) { switch (name) {
case 'tcyun':
return new API.TcyunApi(this.currentPicBedConfig.secretId, this.currentPicBedConfig.secretKey, this.logger)
case 'aliyun': 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 'qiniu':
return new API.QiniuApi(this.currentPicBedConfig.accessKey, this.currentPicBedConfig.secretKey, this.logger)
case 'upyun':
return new API.UpyunApi(this.currentPicBedConfig.bucketName, this.currentPicBedConfig.operator, this.currentPicBedConfig.password, this.logger)
case 'smms':
return new API.SmmsApi(this.currentPicBedConfig.token, this.logger)
case 'github': 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': 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':
return new API.QiniuApi(this.currentPicBedConfig.accessKey, this.currentPicBedConfig.secretKey, this.logger)
case 'smms':
return new API.SmmsApi(this.currentPicBedConfig.token, this.logger)
case 's3plist': 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) 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)
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)
case 'webdavplist': case 'webdavplist':
return new API.WebdavplistApi(this.currentPicBedConfig.endpoint, this.currentPicBedConfig.username, this.currentPicBedConfig.password, this.currentPicBedConfig.sslEnabled, this.currentPicBedConfig.proxy, this.logger) return new API.WebdavplistApi(this.currentPicBedConfig.endpoint, this.currentPicBedConfig.username, this.currentPicBedConfig.password, this.currentPicBedConfig.sslEnabled, this.currentPicBedConfig.proxy, this.logger)
default: default:
@@ -104,9 +108,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
getConfig<T> (name?: string): T { getConfig<T> (name?: string): T {
if (!name) { if (!name) {
return this._config as unknown as T return this._config as unknown as T
} else {
return get(this._config, name)
} }
return get(this._config, name)
} }
saveConfig (config: IStringKeyMap): void { saveConfig (config: IStringKeyMap): void {
@@ -149,6 +152,7 @@ export class ManageApi extends EventEmitter implements ManageApiType {
param?: IStringKeyMap | undefined param?: IStringKeyMap | undefined
): Promise<any> { ): Promise<any> {
let client let client
const name = this.currentPicBedConfig.picBedName.replace('plist', '')
switch (this.currentPicBedConfig.picBedName) { switch (this.currentPicBedConfig.picBedName) {
case 'tcyun': case 'tcyun':
case 'aliyun': case 'aliyun':
@@ -170,15 +174,12 @@ export class ManageApi extends EventEmitter implements ManageApiType {
CreationDate: new Date().toISOString() CreationDate: new Date().toISOString()
}] }]
case 'smms': case 'smms':
return [{
Name: 'smms',
Location: 'smms',
CreationDate: new Date().toISOString()
}]
case 'webdavplist': case 'webdavplist':
case 'local':
case 'sftp':
return [{ return [{
Name: 'webdav', Name: name,
Location: 'webdav', Location: name,
CreationDate: new Date().toISOString() CreationDate: new Date().toISOString()
}] }]
default: default:
@@ -314,6 +315,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'imgur': case 'imgur':
case 's3plist': case 's3plist':
case 'webdavplist': case 'webdavplist':
case 'local':
case 'sftp':
try { try {
client = this.createClient() as any client = this.createClient() as any
return await client.getBucketListRecursively(param!) return await client.getBucketListRecursively(param!)
@@ -357,6 +360,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'imgur': case 'imgur':
case 's3plist': case 's3plist':
case 'webdavplist': case 'webdavplist':
case 'local':
case 'sftp':
try { try {
client = this.createClient() as any client = this.createClient() as any
return await client.getBucketListBackstage(param!) return await client.getBucketListBackstage(param!)
@@ -427,6 +432,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'imgur': case 'imgur':
case 's3plist': case 's3plist':
case 'webdavplist': case 'webdavplist':
case 'local':
case 'sftp':
try { try {
client = this.createClient() as any client = this.createClient() as any
const res = await client.deleteBucketFile(param!) const res = await client.deleteBucketFile(param!)
@@ -452,6 +459,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'github': case 'github':
case 's3plist': case 's3plist':
case 'webdavplist': case 'webdavplist':
case 'local':
case 'sftp':
try { try {
client = this.createClient() as any client = this.createClient() as any
return await client.deleteBucketFolder(param!) return await client.deleteBucketFolder(param!)
@@ -475,6 +484,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'upyun': case 'upyun':
case 's3plist': case 's3plist':
case 'webdavplist': case 'webdavplist':
case 'local':
case 'sftp':
try { try {
client = this.createClient() as any client = this.createClient() as any
return await client.renameBucketFile(param!) return await client.renameBucketFile(param!)
@@ -501,6 +512,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'imgur': case 'imgur':
case 's3plist': case 's3plist':
case 'webdavplist': case 'webdavplist':
case 'local':
case 'sftp':
try { try {
client = this.createClient() as any client = this.createClient() as any
const res = await client.downloadBucketFile(param!) const res = await client.downloadBucketFile(param!)
@@ -533,6 +546,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'github': case 'github':
case 's3plist': case 's3plist':
case 'webdavplist': case 'webdavplist':
case 'local':
case 'sftp':
try { try {
client = this.createClient() as any client = this.createClient() as any
return await client.createBucketFolder(param!) return await client.createBucketFolder(param!)
@@ -559,6 +574,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'imgur': case 'imgur':
case 's3plist': case 's3plist':
case 'webdavplist': case 'webdavplist':
case 'local':
case 'sftp':
try { try {
client = this.createClient() as any client = this.createClient() as any
return await client.uploadBucketFile(param!) return await client.uploadBucketFile(param!)
+66 -82
View File
@@ -4,7 +4,7 @@ import mime from 'mime-types'
import axios from 'axios' import axios from 'axios'
import { app } from 'electron' import { app } from 'electron'
import crypto from 'crypto' import crypto from 'crypto'
import got, { RequestError } from 'got' import got, { OptionsOfTextResponseBody, RequestError } from 'got'
import { Stream } from 'stream' import { Stream } from 'stream'
import { promisify } from 'util' import { promisify } from 'util'
import UpDownTaskQueue, import UpDownTaskQueue,
@@ -40,20 +40,13 @@ export const getFSFile = async (
} }
} }
export const isInputConfigValid = (config: any): boolean => { export function isInputConfigValid (config: any): boolean {
if ( return typeof config === 'object' &&
typeof config === 'object' &&
!Array.isArray(config) && !Array.isArray(config) &&
Object.keys(config).length > 0 Object.keys(config).length > 0
) {
return true
}
return false
} }
export const getFileMimeType = (filePath: string): string => { export const getFileMimeType = (filePath: string): string => mime.lookup(filePath) || 'application/octet-stream'
return mime.lookup(filePath) || 'application/octet-stream'
}
const checkTempFolderExist = async () => { const checkTempFolderExist = async () => {
const tempPath = path.join(app.getPath('downloads'), 'piclistTemp') const tempPath = path.join(app.getPath('downloads'), 'piclistTemp')
@@ -131,7 +124,7 @@ export const NewDownloader = async (
}) })
return true return true
} catch (e: any) { } catch (e: any) {
logger && logger.error(formatError(e, { method: 'NewDownloader' })) logger?.error(formatError(e, { method: 'NewDownloader' }))
fs.remove(savedFilePath) fs.remove(savedFilePath)
instance.updateDownloadTask({ instance.updateDownloadTask({
id, id,
@@ -179,13 +172,13 @@ export const gotUpload = async (
.then((res: any) => { .then((res: any) => {
instance.updateUploadTask({ instance.updateUploadTask({
id, id,
progress: res && (res.statusCode === 200 || res.statusCode === 201) ? 100 : 0, progress: res?.statusCode === 200 || res?.statusCode === 201 ? 100 : 0,
status: res && (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() finishTime: new Date().toLocaleString()
}) })
}) })
.catch((err: any) => { .catch((err: any) => {
logger && logger.error(formatError(err, { method: 'gotUpload' })) logger?.error(formatError(err, { method: 'gotUpload' }))
instance.updateUploadTask({ instance.updateUploadTask({
id, id,
progress: 0, progress: 0,
@@ -213,42 +206,46 @@ export const formatError = (err: any, params:IStringKeyMap) => {
message: err.message ?? '', message: err.message ?? '',
stack: err.stack ?? '' stack: err.stack ?? ''
} }
} else {
if (typeof err === 'object') {
return JSON.stringify(err) + JSON.stringify(params)
} else {
return String(err) + JSON.stringify(params)
}
} }
if (typeof err === 'object') {
return `${JSON.stringify(err)}${JSON.stringify(params)}`
}
return `${String(err)}${JSON.stringify(params)}`
} }
export const trimPath = (path: string) => path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/') export const trimPath = (path: string) => path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/')
export const getAgent = (proxy:any, https: boolean = true) => { const commonOptions = {
keepAlive: true,
keepAliveMsecs: 1000,
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined
} as any
export const getAgent = (proxy:any, https: boolean = true): {
https?: HttpsProxyAgent
http?: HttpProxyAgent
} => {
const formatProxy = formatHttpProxy(proxy, 'string') as any const formatProxy = formatHttpProxy(proxy, 'string') as any
const commonResult = {
https: undefined,
http: undefined
}
if (!formatProxy) return commonResult
commonOptions.proxy = formatProxy.replace('127.0.0.1', 'localhost')
if (https) { if (https) {
return formatProxy return {
? { https: new HttpsProxyAgent({
https: new HttpsProxyAgent({ ...commonOptions,
keepAlive: true, rejectUnauthorized: false
keepAliveMsecs: 1000, }),
rejectUnauthorized: false, http: undefined
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined, }
proxy: formatProxy.replace('127.0.0.1', 'localhost') }
}) return {
} http: new HttpProxyAgent({
: {} ...commonOptions
} else { }),
return formatProxy https: undefined
? {
http: new HttpProxyAgent({
keepAlive: true,
keepAliveMsecs: 1000,
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined,
proxy: formatProxy.replace('127.0.0.1', 'localhost')
})
}
: {}
} }
} }
@@ -258,10 +255,8 @@ export const getInnerAgent = (proxy: any, sslEnabled: boolean = true) => {
return formatProxy return formatProxy
? { ? {
agent: new https.Agent({ agent: new https.Agent({
keepAlive: true, ...commonOptions,
keepAliveMsecs: 1000,
rejectUnauthorized: false, rejectUnauthorized: false,
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined,
host: formatProxy.host, host: formatProxy.host,
port: formatProxy.port port: formatProxy.port
}) })
@@ -272,25 +267,20 @@ export const getInnerAgent = (proxy: any, sslEnabled: boolean = true) => {
keepAlive: true keepAlive: true
}) })
} }
} else {
return formatProxy
? {
agent: new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined,
host: formatProxy.host,
port: formatProxy.port
})
}
: {
agent: new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined
})
}
} }
return formatProxy
? {
agent: new http.Agent({
...commonOptions,
host: formatProxy.host,
port: formatProxy.port
})
}
: {
agent: new http.Agent({
...commonOptions
})
}
} }
export function getOptions ( export function getOptions (
@@ -301,23 +291,17 @@ export function getOptions (
body?: any, body?: any,
timeout?: number, timeout?: number,
proxy?: any proxy?: any
) { ): OptionsOfTextResponseBody {
const options = { return {
method: method?.toUpperCase(), ...(method && { method: method.toUpperCase() }),
headers, ...(headers && { headers }),
searchParams, ...(searchParams && { searchParams }),
agent: getAgent(proxy), ...(body && { body }),
timeout: { ...(responseType && { responseType }),
request: timeout || 30000 ...(timeout !== undefined ? { timeout: { request: timeout } } : { timeout: { request: 30000 } }),
}, ...(proxy && { agent: Object.fromEntries(Object.entries(getAgent(proxy)).filter(([, v]) => v !== undefined)) }),
body, throwHttpErrors: false
throwHttpErrors: false, }
responseType
} as IStringKeyMap
Object.keys(options).forEach(key => {
options[key] === undefined && delete options[key]
})
return options
} }
export const formatEndpoint = (endpoint: string, sslEnabled: boolean): string => export const formatEndpoint = (endpoint: string, sslEnabled: boolean): string =>
+29 -29
View File
@@ -1,32 +1,33 @@
const AliyunAreaCodeName : IStringKeyMap = { const AliyunAreaCodeName : IStringKeyMap = {
'oss-cn-hangzhou': '华东1杭州', 'oss-cn-hangzhou': '华东1(杭州)',
'oss-cn-shanghai': '华东2上海', 'oss-cn-shanghai': '华东2(上海)',
'oss-cn-nanjing': '华东5南京本地地域)', 'oss-cn-nanjing': '华东5(南京)',
'oss-cn-fuzhou': '华东6福州本地地域)', 'oss-cn-fuzhou': '华东6(福州)',
'oss-cn-qingdao': '华北1青岛', 'oss-cn-qingdao': '华北1(青岛)',
'oss-cn-beijing': '华北2北京', 'oss-cn-beijing': '华北2(北京)',
'oss-cn-zhangjiakou': '华北3张家口', 'oss-cn-zhangjiakou': '华北3(张家口)',
'oss-cn-huhehaote': '华北5呼和浩特', 'oss-cn-huhehaote': '华北5(呼和浩特)',
'oss-cn-wulanchabu': '华北6乌兰察布', 'oss-cn-wulanchabu': '华北6(乌兰察布)',
'oss-cn-shenzhen': '华南1深圳', 'oss-cn-shenzhen': '华南1(深圳)',
'oss-cn-heyuan': '华南2河源', 'oss-cn-heyuan': '华南2(河源)',
'oss-cn-guangzhou': '华南3广州', 'oss-cn-guangzhou': '华南3(广州)',
'oss-cn-chengdu': '西南1成都', 'oss-cn-chengdu': '西南1(成都)',
'oss-cn-hongkong': '中国香港', 'oss-cn-hongkong': '中国香港',
'oss-us-west-1': '美国硅谷', 'oss-us-west-1': '美国(硅谷)',
'oss-us-east-1': '美国弗吉尼亚', 'oss-us-east-1': '美国(弗吉尼亚)',
'oss-ap-northeast-1': '日本东京', 'oss-ap-northeast-1': '日本(东京)',
'oss-ap-northeast-2': '韩国首尔', 'oss-ap-northeast-2': '韩国(首尔)',
'oss-ap-southeast-1': '新加坡', 'oss-ap-southeast-1': '新加坡',
'oss-ap-southeast-2': '澳大利亚悉尼', 'oss-ap-southeast-2': '澳大利亚(悉尼)',
'oss-ap-southeast-3': '马来西亚吉隆坡', 'oss-ap-southeast-3': '马来西亚(吉隆坡)',
'oss-ap-southeast-5': '印度尼西亚雅加达', 'oss-ap-southeast-5': '印度尼西亚(雅加达)',
'oss-ap-southeast-6': '菲律宾马尼拉', 'oss-ap-southeast-6': '菲律宾(马尼拉)',
'oss-ap-southeast-7': '泰国曼谷', 'oss-ap-southeast-7': '泰国(曼谷)',
'oss-ap-south-1': '印度孟买', 'oss-ap-south-1': '印度(孟买)',
'oss-eu-central-1': '德国法兰克福', 'oss-eu-central-1': '德国(法兰克福)',
'oss-eu-west-1': '英国伦敦', 'oss-eu-west-1': '英国(伦敦)',
'oss-me-east-1': '阿联酋迪拜' 'oss-me-east-1': '阿联酋(迪拜)',
'oss-rg-china-mainland': '无地域属性'
} }
const QiniuAreaCodeName : IStringKeyMap = { const QiniuAreaCodeName : IStringKeyMap = {
@@ -61,8 +62,7 @@ const TencentAreaCodeName : IStringKeyMap = {
'na-ashburn': '弗吉尼亚(美东)', 'na-ashburn': '弗吉尼亚(美东)',
'na-toronto': '多伦多', 'na-toronto': '多伦多',
'sa-saopaulo': '圣保罗', 'sa-saopaulo': '圣保罗',
'eu-frankfurt': '法兰克福', 'eu-frankfurt': '法兰克福'
'eu-moscow': '莫斯科'
} }
export { AliyunAreaCodeName, QiniuAreaCodeName, TencentAreaCodeName } export { AliyunAreaCodeName, QiniuAreaCodeName, TencentAreaCodeName }
+65
View File
@@ -0,0 +1,65 @@
import axios from 'axios'
import crypto from 'crypto'
import querystring from 'querystring'
import picgo from '@core/picgo'
export interface DogecloudToken {
accessKeyId: string
secretAccessKey: string
sessionToken: string
}
export async function dogecloudApi (
apiPath: string,
data = {},
jsonMode: boolean = false,
accessKey: string,
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 authorization = `TOKEN ${accessKey}:${sign}`
try {
const res = await axios.request({
url: 'https://api.dogecloud.com' + apiPath,
method: 'POST',
data: body,
responseType: 'json',
headers: {
'Content-Type': jsonMode ? 'application/json' : 'application/x-www-form-urlencoded',
Authorization: authorization
}
})
if (res.data.code !== 200) {
throw new Error('API Error')
}
return res.data.data
} catch (err: any) {
throw new Error('API Error')
}
}
export async function getTempToken (accessKey: string, secretKey: string): Promise<{} | 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 token = data.Credentials
picgo.saveConfig({
Credentials: {
'doge-token': {
token,
expires: data.ExpiredAt * 1000
}
}
})
return token
} catch (err: any) {
return {}
}
}
+19 -25
View File
@@ -92,30 +92,9 @@ export class ManageLogger implements ILogger {
): void { ): void {
try { try {
if (this.checkLogLevel(type, this.logLevel)) { if (this.checkLogLevel(type, this.logLevel)) {
let log = `${dayjs().format( let log = `${dayjs().format('YYYY-MM-DD HH:mm:ss')} [PicList ${type.toUpperCase()}] `
'YYYY-MM-DD HH:mm:ss'
)} [PicList ${type.toUpperCase()}] `
msg.forEach((item: ILogArgvTypeWithError) => { msg.forEach((item: ILogArgvTypeWithError) => {
if (item instanceof Error && type === 'error') { log += this.formatLogItem(item, type)
log += `\n------Error Stack Begin------\n${util.format(
item?.stack
)}\n-------Error Stack End------- `
} else {
if (typeof item === 'object') {
if (item?.stack) {
log = log + `\n------Error Stack Begin------\n${util.format(
item.stack
)}\n-------Error Stack End------- `
}
item = JSON.stringify(item, (key, value) => {
if (key === 'stack') {
return undefined
}
return value
}, 2)
}
log += `${item as string} `
}
}) })
log += '\n' log += '\n'
fs.appendFileSync(logPath, log) fs.appendFileSync(logPath, log)
@@ -125,6 +104,22 @@ export class ManageLogger implements ILogger {
} }
} }
private 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------- `
} else {
if (typeof item === 'object') {
if (item?.stack) {
result += `\n------Error Stack Begin------\n${util.format(item.stack)}\n-------Error Stack End------- `
}
item = JSON.stringify(item, (key, value) => (key === 'stack' ? undefined : value), 2)
}
result += `${item as string} `
}
return result
}
private checkLogLevel ( private checkLogLevel (
type: string, type: string,
level: undefined | string | string[] level: undefined | string | string[]
@@ -134,9 +129,8 @@ export class ManageLogger implements ILogger {
} }
if (Array.isArray(level)) { if (Array.isArray(level)) {
return level.some((item: string) => item === type || item === 'all') return level.some((item: string) => item === type || item === 'all')
} else {
return type === level
} }
return type === level
} }
success (...msq: ILogArgvType[]): void { success (...msq: ILogArgvType[]): void {
-61
View File
@@ -1,61 +0,0 @@
import { DBStore } from '@picgo/store'
import ConfigStore from '~/main/apis/core/datastore'
import path from 'path'
import fse from 'fs-extra'
import { PicGo as PicGoCore } from 'piclist'
import { T } from '~/main/i18n'
// from v2.1.2
const updateShortKeyFromVersion212 = (db: typeof ConfigStore, shortKeyConfig: IShortKeyConfigs | IOldShortKeyConfigs) => {
// #557 极端情况可能会出现配置不存在,需要重新写入
if (shortKeyConfig === undefined) {
const defaultShortKeyConfig = {
enable: true,
key: 'CommandOrControl+Shift+P',
name: 'upload',
label: T('QUICK_UPLOAD')
}
db.set('settings.shortKey[picgo:upload]', defaultShortKeyConfig)
return true
}
if (shortKeyConfig.upload) {
// @ts-ignore
shortKeyConfig['picgo:upload'] = {
enable: true,
key: shortKeyConfig.upload,
name: 'upload',
label: T('QUICK_UPLOAD')
}
// @ts-ignore
delete shortKeyConfig.upload
db.set('settings.shortKey', shortKeyConfig)
return true
}
return false
}
const migrateGalleryFromVersion230 = async (configDB: typeof ConfigStore, galleryDB: DBStore, picgo: PicGoCore) => {
const originGallery: ImgInfo[] = picgo.getConfig('uploaded')
// if hasMigrate, we don't need to migrate
const hasMigrate: boolean = configDB.get('__migrateUploaded')
if (hasMigrate) {
return
}
const configPath = configDB.getConfigPath()
const configBakPath = path.join(path.dirname(configPath), 'config.bak.json')
// migrate gallery from config to gallery db
if (originGallery && Array.isArray(originGallery) && originGallery?.length > 0) {
if (fse.existsSync(configBakPath)) {
fse.copyFileSync(configPath, configBakPath)
}
await galleryDB.insertMany(originGallery)
picgo.saveConfig({
uploaded: [],
__migrateUploaded: true
})
}
}
export {
updateShortKeyFromVersion212,
migrateGalleryFromVersion230
}
+2 -5
View File
@@ -11,6 +11,7 @@ import axios from 'axios'
class Server { class Server {
private httpServer: http.Server private httpServer: http.Server
private config: IServerConfig private config: IServerConfig
constructor () { constructor () {
let config = picgo.getConfig<IServerConfig>('settings.server') let config = picgo.getConfig<IServerConfig>('settings.server')
const result = this.checkIfConfigIsValid(config) const result = this.checkIfConfigIsValid(config)
@@ -31,11 +32,7 @@ class Server {
} }
private checkIfConfigIsValid (config: IObj | undefined) { private checkIfConfigIsValid (config: IObj | undefined) {
if (config && config.port && config.host && (config.enable !== undefined)) { return config && config.port && config.host && (config.enable !== undefined)
return true
} else {
return false
}
} }
private handleRequest = (request: http.IncomingMessage, response: http.ServerResponse) => { private handleRequest = (request: http.IncomingMessage, response: http.ServerResponse) => {
+1 -1
View File
@@ -7,9 +7,9 @@ import windowManager from 'apis/app/window/windowManager'
import { uploadChoosedFiles, uploadClipboardFiles, deleteChoosedFiles } from 'apis/app/uploader/apis' import { uploadChoosedFiles, uploadClipboardFiles, deleteChoosedFiles } from 'apis/app/uploader/apis'
import path from 'path' import path from 'path'
import { dbPathDir } from 'apis/core/datastore/dbChecker' import { dbPathDir } from 'apis/core/datastore/dbChecker'
const STORE_PATH = dbPathDir() const STORE_PATH = dbPathDir()
const LOG_PATH = path.join(STORE_PATH, 'piclist.log') const LOG_PATH = path.join(STORE_PATH, 'piclist.log')
// import AllAPI from '../../renderer/apis/allApi'
const errorMessage = `upload error. see ${LOG_PATH} for more detail.` const errorMessage = `upload error. see ${LOG_PATH} for more detail.`
const deleteErrorMessage = `delete error. see ${LOG_PATH} for more detail.` const deleteErrorMessage = `delete error. see ${LOG_PATH} for more detail.`
+5 -8
View File
@@ -21,14 +21,11 @@ function beforeOpen () {
*/ */
function resolveMacWorkFlow () { function resolveMacWorkFlow () {
const dest = `${os.homedir()}/Library/Services/Upload pictures with PicList.workflow` const dest = `${os.homedir()}/Library/Services/Upload pictures with PicList.workflow`
if (fs.existsSync(dest)) { if (fs.existsSync(dest)) return true
return true try {
} else { fs.copySync(path.join(__static, 'Upload pictures with PicList.workflow'), dest)
try { } catch (e) {
fs.copySync(path.join(__static, 'Upload pictures with PicList.workflow'), dest) console.log(e)
} catch (e) {
console.log(e)
}
} }
} }
+199
View File
@@ -0,0 +1,199 @@
import { S3Client, DeleteObjectCommand, S3ClientConfig } from '@aws-sdk/client-s3'
import { NodeHttpHandler } from '@smithy/node-http-handler'
import http, { AgentOptions } from 'http'
import https from 'https'
import { getAgent } from '../manage/utils/common'
import axios from 'axios'
import crypto from 'crypto'
import querystring from 'querystring'
interface DogecloudTokenFull {
Credentials: {
accessKeyId: string
secretAccessKey: string
sessionToken: string
},
ExpiredAt: number,
Buckets: {
name: string
s3Bucket: string
s3Endpoint: string
}[]
}
const dogeRegionMap: IStringKeyMap = {
'ap-shanghai': '0',
'ap-beijing': '1',
'ap-guangzhou': '2',
'ap-chengdu': '3'
}
async function dogecloudApi (
apiPath: string,
data = {},
jsonMode: boolean = false,
accessKey: string,
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 authorization = `TOKEN ${accessKey}:${sign}`
try {
const res = await axios.request({
url: `https://api.dogecloud.com${apiPath}`,
method: 'POST',
data: body,
responseType: 'json',
headers: {
'Content-Type': jsonMode ? 'application/json' : 'application/x-www-form-urlencoded',
Authorization: authorization
}
})
if (res.data.code !== 200) {
throw new Error('API Error')
}
return res.data.data
} catch (err: any) {
throw new Error('API Error')
}
}
async function getDogeToken (accessKey: string, secretKey: string): Promise<{} | DogecloudTokenFull> {
try {
const data = await dogecloudApi('/auth/tmp_token.json', {
channel: 'OSS_FULL',
scopes: ['*']
}, true, accessKey, secretKey)
return data
} catch (err: any) {
console.log(err)
return {}
}
}
export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode: boolean = false) {
try {
const { imgUrl, config: { accessKeyID, secretAccessKey, bucketName, region, endpoint, pathStyleAccess, rejectUnauthorized, proxy } } = configMap
const url = new URL(!/^https?:\/\//.test(imgUrl) ? `http://${imgUrl}` : imgUrl)
const fileKey = url.pathname.replace(/^\/+/, '')
const endpointUrl: string | undefined = endpoint
? /^https?:\/\//.test(endpoint)
? endpoint
: `http://${endpoint}`
: undefined
const sslEnabled = endpointUrl ? endpointUrl.startsWith('https') : true
const agent = getAgent(proxy, sslEnabled)
const commonOptions: AgentOptions = {
keepAlive: true,
keepAliveMsecs: 1000,
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined
}
const extraOptions = sslEnabled ? { rejectUnauthorized: !!rejectUnauthorized } : {}
const handler = sslEnabled
? new NodeHttpHandler({
httpsAgent: agent.https
? agent.https
: new https.Agent({
...commonOptions,
...extraOptions
})
})
: new NodeHttpHandler({
httpAgent: agent.http
? agent.http
: new http.Agent({
...commonOptions,
...extraOptions
})
})
const s3Options: S3ClientConfig = {
credentials: {
accessKeyId: accessKeyID,
secretAccessKey
},
endpoint: endpointUrl,
tls: sslEnabled,
forcePathStyle: pathStyleAccess,
region,
requestHandler: handler
}
if (dogeMode) {
s3Options.credentials = {
accessKeyId: configMap.config.accessKeyID,
secretAccessKey: configMap.config.secretAccessKey,
sessionToken: configMap.config.sessionToken
}
}
const client = new S3Client(s3Options)
const command = new DeleteObjectCommand({
Bucket: bucketName,
Key: fileKey
})
const result = await client.send(command)
return result.$metadata.httpStatusCode === 204
} catch (err: any) {
console.log(err)
return false
}
}
export async function removeFileFromDogeInMain (configMap: IStringKeyMap) {
try {
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 = {
...newConfigMap.config,
accessKeyID: token.Credentials?.accessKeyId,
secretAccessKey: token.Credentials?.secretAccessKey,
sessionToken: token.Credentials?.sessionToken,
endpoint: bucket?.s3Endpoint,
region: dogeRegionMap[bucket?.s3Endpoint?.split('.')[1] || 'ap-shanghai'],
bucketName: bucket?.s3Bucket
}
return await removeFileFromS3InMain(newConfigMap, true)
} catch (err: any) {
console.log(err)
return false
}
}
function createHuaweiAuthorization (
bucketName: string,
path: string,
fileName: string,
accessKey: string,
secretKey: string,
date: string = new Date().toUTCString()
) {
const strToSign = `DELETE\n\n\n${date}\n/${bucketName}${path}/${fileName}`
const singature = crypto.createHmac('sha1', secretKey).update(strToSign).digest('base64')
return `OBS ${accessKey}:${singature}`
}
export async function removeFileFromHuaweiInMain (configMap: IStringKeyMap) {
const { fileName, config } = configMap
const { accessKeyId, accessKeySecret, bucketName, endpoint } = config
let path = config.path || '/'
path = `/${path.replace(/^\/+|\/+$/, '')}`
path = path === '/' ? '' : path
const date = new Date().toUTCString()
const authorization = createHuaweiAuthorization(bucketName, path, fileName, accessKeyId, accessKeySecret, date)
try {
const res = await axios.request({
url: `https://${bucketName}.${endpoint}${encodeURI(path)}/${encodeURIComponent(fileName)}`,
method: 'DELETE',
responseType: 'json',
headers: {
Host: `${bucketName}.${endpoint}`,
Date: date,
Authorization: authorization
}
})
return res.status === 204
} catch (error) {
console.log(error)
return false
}
}
+147
View File
@@ -0,0 +1,147 @@
import { NodeSSH, Config, SSHExecCommandResponse } from 'node-ssh-no-cpu-features'
import path from 'path'
import { ISftpPlistConfig } from 'piclist/dist/types'
class SSHClient {
// eslint-disable-next-line no-use-before-define
private static _instance: SSHClient
private static _client: NodeSSH
private _isConnected = false
public static get instance (): SSHClient {
return this._instance || (this._instance = new this())
}
public static get client (): NodeSSH {
return this._client || (this._client = new NodeSSH())
}
private changeWinStylePathToUnix (path: string): string {
return path.replace(/\\/g, '/')
}
public async connect (config: ISftpPlistConfig): Promise<boolean> {
const { username, password, privateKey, passphrase } = config
const loginInfo: Config = privateKey
? { username, privateKeyPath: privateKey, passphrase: passphrase || undefined }
: { username, password }
try {
await SSHClient.client.connect({
host: config.host,
port: Number(config.port) || 22,
...loginInfo
})
this._isConnected = true
return true
} catch (err: any) {
throw new Error(err)
}
}
public async deleteFile (remote: string): Promise<boolean> {
if (!this._isConnected) {
throw new Error('SSH 未连接')
}
try {
remote = this.changeWinStylePathToUnix(remote)
if (remote === '/' || remote.includes('*')) return false
const script = `rm -f "${remote}"`
return await this.exec(script)
} catch (err: any) {
return false
}
}
private async exec (script: string): Promise<boolean> {
const execResult = await SSHClient.client.execCommand(script)
return execResult.code === 0
}
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> {
if (!this._isConnected) {
throw new Error('SSH 未连接')
}
try {
remote = this.changeWinStylePathToUnix(remote)
local = this.changeWinStylePathToUnix(local)
await SSHClient.client.getFile(local, remote, undefined, {
concurrency: 1
})
return true
} catch (err: any) {
console.log(err)
return false
}
}
async putFile (local: string, remote: string, config: {
fileMode?: string
dirMode?: string
} = {}): Promise<boolean> {
if (!this._isConnected) {
throw new Error('SSH 未连接')
}
try {
remote = this.changeWinStylePathToUnix(remote)
await this.mkdir(path.dirname(remote).replace(/^\/+|\/+$/g, ''), config)
await SSHClient.client.putFile(local, remote)
const fileMode = config.fileMode || '0644'
if (fileMode !== '0644') {
const script = `chmod ${fileMode} "${remote}"`
return await this.exec(script)
}
return true
} catch (err: any) {
console.log(err)
return false
}
}
async mkdir (dirPath: string, config: {
dirMode?: string
} = {}): Promise<boolean> {
if (!this._isConnected) {
throw new Error('SSH 未连接')
}
try {
const directoryMode = config.dirMode || '0755'
if (directoryMode === '0755') {
const script = `mkdir -p "${dirPath}"`
return await this.exec(script)
} else {
const dirs = dirPath.split('/')
let currentPath = ''
for (const dir of dirs) {
if (dir) {
currentPath += `/${dir}`
const script = `mkdir "${currentPath}" && chmod ${directoryMode} "${currentPath}"`
const result = await this.exec(script)
if (!result) {
return false
}
}
}
return true
}
} catch (err: any) {
console.log(err)
return false
}
}
get isConnected (): boolean {
return SSHClient.client.isConnected()
}
public close (): void {
SSHClient.client.dispose()
this._isConnected = false
}
}
export default SSHClient
+9
View File
@@ -5,10 +5,19 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// 状态管理
import { useStore } from '@/hooks/useStore' import { useStore } from '@/hooks/useStore'
// Vue 生命周期钩子
import { onBeforeMount, onMounted, onUnmounted } from 'vue' import { onBeforeMount, onMounted, onUnmounted } from 'vue'
// 数据发送工具函数
import { getConfig } from './utils/dataSender' import { getConfig } from './utils/dataSender'
// 类型声明
import type { IConfig } from 'piclist' import type { IConfig } from 'piclist'
// 其他工具
import bus from './utils/bus' import bus from './utils/bus'
import { FORCE_UPDATE } from '~/universal/events/constants' import { FORCE_UPDATE } from '~/universal/events/constants'
+28
View File
@@ -0,0 +1,28 @@
import axios from 'axios'
import path from 'path'
export default class AlistApi {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
const { fileName, config } = configMap
try {
const { version, url, uploadPath, token } = config
if (String(version) === '2') return true
const result = await axios.request({
method: 'post',
url: `${url}/api/fs/remove`,
headers: {
'Content-Type': 'application/json',
Authorization: token
},
data: {
dir: path.join('/', uploadPath, path.dirname(fileName)),
names: [path.basename(fileName)]
}
})
return result.data.code === 200
} catch (error) {
console.error(error)
return false
}
}
}
+2 -2
View File
@@ -24,7 +24,7 @@ export default class AliyunApi {
private static getKey (fileName: string, path?: string): string { private static getKey (fileName: string, path?: string): string {
return path && path !== '/' return path && path !== '/'
? `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}` ? `${path.replace(/^\/+|\/+$/, '')}/${fileName}`
: fileName : fileName
} }
@@ -33,7 +33,7 @@ export default class AliyunApi {
try { try {
const client = AliyunApi.createClient(config) const client = AliyunApi.createClient(config)
const key = AliyunApi.getKey(fileName, config.path) const key = AliyunApi.getKey(fileName, config.path)
const result = await client.delete(key) as any const result = await client.delete(key)
return result.res.status === 204 return result.res.status === 204
} catch (error) { } catch (error) {
console.error(error) console.error(error)
+21 -16
View File
@@ -1,33 +1,38 @@
import AliyunApi from './aliyun'
import AwsS3Api from './awss3'
import GithubApi from './github'
import ImgurApi from './imgur'
import LocalApi from './local'
import QiniuApi from './qiniu'
import SftpPlistApi from './sftpplist'
import SmmsApi from './smms' import SmmsApi from './smms'
import TcyunApi from './tcyun' import TcyunApi from './tcyun'
import AliyunApi from './aliyun'
import QiniuApi from './qiniu'
import ImgurApi from './imgur'
import GithubApi from './github'
import UpyunApi from './upyun' import UpyunApi from './upyun'
import AwsS3Api from './awss3'
import WebdavApi from './webdav' import WebdavApi from './webdav'
import LocalApi from './local' import DogeCloudApi from './dogecloud'
import HuaweicloudApi from './huaweiyun'
import AlistApi from './alist'
const apiMap: IStringKeyMap = { const apiMap: IStringKeyMap = {
aliyun: AliyunApi,
'aws-s3': AwsS3Api,
github: GithubApi,
imgur: ImgurApi,
local: LocalApi,
qiniu: QiniuApi,
sftpplist: SftpPlistApi,
smms: SmmsApi, smms: SmmsApi,
tcyun: TcyunApi, tcyun: TcyunApi,
aliyun: AliyunApi,
qiniu: QiniuApi,
imgur: ImgurApi,
github: GithubApi,
upyun: UpyunApi, upyun: UpyunApi,
'aws-s3': AwsS3Api,
webdavplist: WebdavApi, webdavplist: WebdavApi,
local: LocalApi dogecloud: DogeCloudApi,
'huaweicloud-uploader': HuaweicloudApi,
alist: AlistApi
} }
export default class ALLApi { export default class ALLApi {
static async delete (configMap: IStringKeyMap): Promise<boolean> { static async delete (configMap: IStringKeyMap): Promise<boolean> {
const api = apiMap[configMap.type] const api = apiMap[configMap.type]
if (api) { return api ? await api.delete(configMap) : false
return await api.delete(configMap)
}
return false
} }
} }
+8 -37
View File
@@ -1,44 +1,15 @@
import { S3 } from 'aws-sdk' import { ipcRenderer } from 'electron'
import { getRawData } from '~/renderer/utils/common'
import { removeFileFromS3InMain } from '~/main/utils/deleteFunc'
export default class AwsS3Api { export default class AwsS3Api {
static async delete (configMap: IStringKeyMap): Promise<boolean> { static async delete (configMap: IStringKeyMap): Promise<boolean> {
const { imgUrl, config: { accessKeyID, secretAccessKey, bucketName, region, endpoint, pathStyleAccess, bucketEndpoint, rejectUnauthorized } } = configMap
try { try {
const url = new URL(!/^https?:\/\//.test(imgUrl) ? `http://${imgUrl}` : imgUrl) return ipcRenderer
const fileKey = url.pathname ? await ipcRenderer.invoke('delete-aws-s3-file',
let endpointUrl getRawData(configMap)
if (endpoint) { )
if (!/^https?:\/\//.test(endpoint)) { : await removeFileFromS3InMain(getRawData(configMap))
endpointUrl = `http://${endpoint}`
} else {
endpointUrl = endpoint
}
}
let sslEnabled = true
if (endpointUrl) {
sslEnabled = endpointUrl.startsWith('https')
}
const http = sslEnabled ? require('https') : require('http')
const client = new S3({
accessKeyId: accessKeyID,
secretAccessKey,
endpoint: endpointUrl,
s3ForcePathStyle: pathStyleAccess,
sslEnabled,
region,
s3BucketEndpoint: bucketEndpoint,
httpOptions: {
agent: new http.Agent({
rejectUnauthorized,
timeout: 30000
})
}
})
const result = await client.deleteObject({
Bucket: bucketName,
Key: fileKey.replace(/^\//, '')
}).promise()
return result.$response.httpResponse.statusCode === 204
} catch (error) { } catch (error) {
console.log(error) console.log(error)
return false return false
+18
View File
@@ -0,0 +1,18 @@
import { ipcRenderer } from 'electron'
import { getRawData } from '~/renderer/utils/common'
import { removeFileFromDogeInMain } from '~/main/utils/deleteFunc'
export default class AwsS3Api {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
try {
return ipcRenderer
? await ipcRenderer.invoke('delete-doge-file',
getRawData(configMap)
)
: await removeFileFromDogeInMain(getRawData(configMap))
} catch (error) {
console.log(error)
return false
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ export default class GithubApi {
private static createKey (path: string | undefined, fileName: string): string { private static createKey (path: string | undefined, fileName: string): string {
return path && path !== '/' return path && path !== '/'
? `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}` ? `${path.replace(/^\/+|\/+$/, '')}/${fileName}`
: fileName : fileName
} }
+18
View File
@@ -0,0 +1,18 @@
import { ipcRenderer } from 'electron'
import { getRawData } from '~/renderer/utils/common'
import { removeFileFromHuaweiInMain } from '~/main/utils/deleteFunc'
export default class HuaweicloudApi {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
try {
return ipcRenderer
? await ipcRenderer.invoke('delete-huaweicloud-file',
getRawData(configMap)
)
: await removeFileFromHuaweiInMain(getRawData(configMap))
} catch (error) {
console.log(error)
return false
}
}
}
+11 -11
View File
@@ -17,6 +17,8 @@ interface IConfig {
} }
export default class ImgurApi { export default class ImgurApi {
static baseUrl: 'https://api.imgur.com/3'
private static async makeRequest ( private static async makeRequest (
method: 'delete', method: 'delete',
url: string, url: string,
@@ -35,25 +37,23 @@ export default class ImgurApi {
} }
static async delete (configMap: IConfigMap): Promise<boolean> { static async delete (configMap: IConfigMap): Promise<boolean> {
const { config = {}, hash = '' } = configMap || {} const {
const { clientId = '', username = '', accessToken = '' } = config config: { clientId = '', username = '', accessToken = '' } = {},
const baseUrl = 'https://api.imgur.com/3' hash = ''
let Authorization: string } = configMap
let apiUrl: string let Authorization: string, apiUrl: string
if (username && accessToken) { if (username && accessToken) {
Authorization = `Bearer ${accessToken}` Authorization = `Bearer ${accessToken}`
apiUrl = `${baseUrl}/account/${username}/image/${hash}` apiUrl = `${ImgurApi.baseUrl}/account/${username}/image/${hash}`
} else if (clientId) { } else if (clientId) {
Authorization = `Client-ID ${clientId}` Authorization = `Client-ID ${clientId}`
apiUrl = `${baseUrl}/image/${hash}` apiUrl = `${ImgurApi.baseUrl}/image/${hash}`
} else { } else {
return false return false
} }
const headers = {
Authorization
}
const requestConfig: IConfig = { const requestConfig: IConfig = {
headers, headers: { Authorization },
timeout: 30000 timeout: 30000
} }
return ImgurApi.makeRequest('delete', apiUrl, requestConfig) return ImgurApi.makeRequest('delete', apiUrl, requestConfig)
+1 -1
View File
@@ -8,7 +8,7 @@ export default class LocalApi {
static async delete (configMap: IConfigMap): Promise<boolean> { static async delete (configMap: IConfigMap): Promise<boolean> {
const { hash } = configMap const { hash } = configMap
if (!hash) { if (!hash) {
console.error('SmmsApi.delete: invalid params') console.error('Local.delete: invalid params')
return false return false
} }
+2 -2
View File
@@ -17,7 +17,7 @@ export default class QiniuApi {
const qiniuConfig = new Qiniu.conf.Config() const qiniuConfig = new Qiniu.conf.Config()
try { try {
const bucketManager = new Qiniu.rs.BucketManager(mac, qiniuConfig) const bucketManager = new Qiniu.rs.BucketManager(mac, qiniuConfig)
const formattedPath = path?.replace(/^\//, '').replace(/\/$/, '') || '' const formattedPath = path?.replace(/^\/+|\/+$/, '') || ''
const key = path === '/' || !path ? fileName : `${formattedPath}/${fileName}` const key = path === '/' || !path ? fileName : `${formattedPath}/${fileName}`
const res = await new Promise((resolve, reject) => { const res = await new Promise((resolve, reject) => {
bucketManager.delete(bucket, key, (err, respBody, respInfo) => { bucketManager.delete(bucket, key, (err, respBody, respInfo) => {
@@ -31,7 +31,7 @@ export default class QiniuApi {
} }
}) })
}) as any }) as any
return res && res.respInfo.statusCode === 200 return res?.respInfo?.statusCode === 200
} catch (error) { } catch (error) {
console.error(error) console.error(error)
return false return false
+18
View File
@@ -0,0 +1,18 @@
import { ipcRenderer } from 'electron'
import { getRawData } from '~/renderer/utils/common'
export default class SftpPlistApi {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
const { fileName, config } = configMap
try {
const deleteResult = await ipcRenderer.invoke('delete-sftp-file',
getRawData(config),
fileName
)
return deleteResult
} catch (error) {
console.error(error)
return false
}
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export default class TcyunApi {
if (path === '/' || !path) { if (path === '/' || !path) {
key = `/${fileName}` key = `/${fileName}`
} else { } else {
key = `/${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}` key = `/${path.replace(/^\/+|\/+$/, '')}/${fileName}`
} }
const result = await cos.deleteObject({ const result = await cos.deleteObject({
Bucket: bucket, Bucket: bucket,
+1 -1
View File
@@ -11,7 +11,7 @@ export default class UpyunApi {
if (path === '/' || !path) { if (path === '/' || !path) {
key = fileName key = fileName
} else { } else {
key = `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}` key = `${path.replace(/^\/+|\/+$/, '')}/${fileName}`
} }
return await client.deleteFile(key) return await client.deleteFile(key)
} catch (error) { } catch (error) {
+1 -1
View File
@@ -16,7 +16,7 @@ export default class WebdavApi {
if (path === '/' || !path) { if (path === '/' || !path) {
key = fileName key = fileName
} else { } else {
key = `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}` key = `${path.replace(/^\/+|\/+$/, '')}/${fileName}`
} }
try { try {
await ctx.deleteFile(key) await ctx.deleteFile(key)
+63
View File
@@ -0,0 +1,63 @@
<template>
<el-image
:src="isShowThumbnail && item.isImage ?
base64Image
: require(`../manage/pages/assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
fit="contain"
style="height: 100px;width: 100%;margin: 0 auto;"
>
<template #placeholder>
<el-icon>
<Loading />
</el-icon>
</template>
<template #error>
<el-image
:src="require(`../manage/pages/assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
fit="contain"
style="height: 100px;width: 100%;margin: 0 auto;"
/>
</template>
</el-image>
</template>
<script lang="ts" setup>
import { ref, onBeforeMount } from 'vue'
import { getFileIconPath } from '@/manage/utils/common'
import { Loading } from '@element-plus/icons-vue'
import fs from 'fs-extra'
import mime from 'mime-types'
import path from 'path'
const base64Image = ref('')
const props = defineProps(
{
isShowThumbnail: {
type: Boolean,
required: true
},
item: {
type: Object,
required: true
},
localPath: {
type: String,
required: true
}
}
)
const createBase64Image = async () => {
const filePath = path.normalize(props.localPath)
const base64 = await fs.readFile(filePath, 'base64')
base64Image.value = `data:${mime.lookup(filePath) || 'image/png'};base64,${base64}`
}
onBeforeMount(async () => {
try {
await createBase64Image()
} catch (e) {
console.log(e)
}
})
</script>
+30 -16
View File
@@ -1,8 +1,6 @@
<template> <template>
<el-image <el-image
:src="isShowThumbnail && item.isImage ? :src="imageSource"
base64Url
: require(`../manage/pages/assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
fit="contain" fit="contain"
style="height: 100px;width: 100%;margin: 0 auto;" style="height: 100px;width: 100%;margin: 0 auto;"
> >
@@ -13,7 +11,7 @@
</template> </template>
<template #error> <template #error>
<el-image <el-image
:src="require(`../manage/pages/assets/icons/${getFileIconPath(item.fileName ?? '')}`)" :src="iconPath"
fit="contain" fit="contain"
style="height: 100px;width: 100%;margin: 0 auto;" style="height: 100px;width: 100%;margin: 0 auto;"
/> />
@@ -22,11 +20,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onBeforeMount } from 'vue' import { ref, onMounted, watch, computed } from 'vue'
import { getFileIconPath } from '@/manage/utils/common' import { getFileIconPath } from '@/manage/utils/common'
import { Loading } from '@element-plus/icons-vue' import { Loading } from '@element-plus/icons-vue'
const base64Url = ref('') const base64Url = ref('')
const success = ref(false)
const props = defineProps( const props = defineProps(
{ {
isShowThumbnail: { isShowThumbnail: {
@@ -48,18 +48,32 @@ const props = defineProps(
} }
) )
const urlCreateObjectURL = async () => { const imageSource = computed(() => {
await fetch(props.url, { return (props.isShowThumbnail && props.item.isImage && success.value)
method: 'GET', ? base64Url.value
headers: props.headers : require(`../manage/pages/assets/icons/${getFileIconPath(props.item.fileName ?? '')}`)
}).then(res => res.blob()).then(blob => { })
base64Url.value = URL.createObjectURL(blob)
}).catch(err => { const iconPath = computed(() => require(`../manage/pages/assets/icons/${getFileIconPath(props.item.fileName ?? '')}`))
const fetchImage = async () => {
try {
const res = await fetch(props.url, { method: 'GET', headers: props.headers })
if (res.status >= 200 && res.status < 300) {
const blob = await res.blob()
success.value = true
base64Url.value = URL.createObjectURL(blob)
} else {
throw new Error('Network response was not ok.')
}
} catch (err) {
success.value = false
console.log(err) console.log(err)
}) }
} }
onBeforeMount(async () => { watch(() => [props.url, props.headers], fetchImage, { deep: true })
await urlCreateObjectURL()
}) onMounted(fetchImage)
</script> </script>
@@ -34,6 +34,8 @@ import {
} from '~/universal/events/constants' } from '~/universal/events/constants'
import $bus from '@/utils/bus' import $bus from '@/utils/bus'
import { sendToMain } from '@/utils/dataSender' import { sendToMain } from '@/utils/dataSender'
import { T as $T } from '@/i18n/index'
const inputBoxValue = ref('') const inputBoxValue = ref('')
const showInputBoxVisible = ref(false) const showInputBoxVisible = ref(false)
const inputBoxOptions = reactive({ const inputBoxOptions = reactive({
@@ -10,6 +10,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { IToolboxItemCheckStatus } from '~/universal/types/enum' import { IToolboxItemCheckStatus } from '~/universal/types/enum'
interface IProps { interface IProps {
status: IToolboxItemCheckStatus status: IToolboxItemCheckStatus
value: any value: any
@@ -18,6 +18,7 @@
import { CircleCloseFilled, Loading, SuccessFilled } from '@element-plus/icons-vue' import { CircleCloseFilled, Loading, SuccessFilled } from '@element-plus/icons-vue'
import { computed } from 'vue' import { computed } from 'vue'
import { IToolboxItemCheckStatus } from '~/universal/types/enum' import { IToolboxItemCheckStatus } from '~/universal/types/enum'
interface IProps { interface IProps {
status: IToolboxItemCheckStatus status: IToolboxItemCheckStatus
} }
+49 -4
View File
@@ -47,6 +47,15 @@
</el-icon> </el-icon>
</div> </div>
</div> </div>
<el-progress
v-if="progressShow"
:percentage="progressPercentage"
:stroke-width="7"
:text-inside="true"
:show-text="false"
status="success"
class="progress-bar"
/>
<el-row <el-row
style="padding-top: 22px;" style="padding-top: 22px;"
class="main-content" class="main-content"
@@ -105,7 +114,7 @@
</el-sub-menu> </el-sub-menu>
<el-menu-item :index="routerConfig.SETTING_PAGE"> <el-menu-item :index="routerConfig.SETTING_PAGE">
<el-icon> <el-icon>
<Setting /> <Tools />
</el-icon> </el-icon>
<span>{{ $T('PICLIST_SETTINGS') }}</span> <span>{{ $T('PICLIST_SETTINGS') }}</span>
</el-menu-item> </el-menu-item>
@@ -233,9 +242,9 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// import { Component, Vue, Watch } from 'vue-property-decorator' // Element Plus 图标
import { import {
Setting, Tools,
UploadFilled, UploadFilled,
PictureFilled, PictureFilled,
Menu, Menu,
@@ -248,20 +257,42 @@ import {
Link, Link,
ArrowUpBold ArrowUpBold
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
// Element Plus 消息框组件
import { ElMessage as $message, ElMessageBox } from 'element-plus' import { ElMessage as $message, ElMessageBox } from 'element-plus'
// 国际化函数
import { T as $T } from '@/i18n/index' import { T as $T } from '@/i18n/index'
// Vue 相关
import { ref, onBeforeUnmount, Ref, onBeforeMount, watch, nextTick, reactive } from 'vue' import { ref, onBeforeUnmount, Ref, onBeforeMount, watch, nextTick, reactive } from 'vue'
// Vue Router 相关
import { onBeforeRouteUpdate, useRouter } from 'vue-router' import { onBeforeRouteUpdate, useRouter } from 'vue-router'
// 二维码组件
import QrcodeVue from 'qrcode.vue' import QrcodeVue from 'qrcode.vue'
// Lodash pick 函数
import pick from 'lodash/pick' import pick from 'lodash/pick'
// 根目录 package.json
import pkg from 'root/package.json' import pkg from 'root/package.json'
// 路由配置常量
import * as config from '@/router/config' import * as config from '@/router/config'
// Electron 相关
import { import {
ipcRenderer, ipcRenderer,
IpcRendererEvent, IpcRendererEvent,
clipboard clipboard
} from 'electron' } from 'electron'
// 输入框对话框组件
import InputBoxDialog from '@/components/InputBoxDialog.vue' import InputBoxDialog from '@/components/InputBoxDialog.vue'
// 事件常量
import { import {
MINIMIZE_WINDOW, MINIMIZE_WINDOW,
CLOSE_WINDOW, CLOSE_WINDOW,
@@ -271,8 +302,13 @@ import {
GET_PICBEDS, GET_PICBEDS,
OPEN_URL OPEN_URL
} from '~/universal/events/constants' } from '~/universal/events/constants'
// 数据发送工具函数
import { getConfig, sendToMain } from '@/utils/dataSender' import { getConfig, sendToMain } from '@/utils/dataSender'
// Piclist 配置类型声明
import { IConfig } from 'piclist' import { IConfig } from 'piclist'
const version = ref(process.env.NODE_ENV === 'production' ? pkg.version : 'Dev') const version = ref(process.env.NODE_ENV === 'production' ? pkg.version : 'Dev')
const routerConfig = reactive(config) const routerConfig = reactive(config)
const defaultActive = ref(routerConfig.UPLOAD_PAGE) const defaultActive = ref(routerConfig.UPLOAD_PAGE)
@@ -284,9 +320,11 @@ const qrcodeVisible = ref(false)
const picBedConfigString = ref('') const picBedConfigString = ref('')
const choosedPicBedForQRCode: Ref<string[]> = ref([]) const choosedPicBedForQRCode: Ref<string[]> = ref([])
const isAlwaysOnTop = ref(false) const isAlwaysOnTop = ref(false)
const keepAlivePages = $router.getRoutes().filter(item => item.meta.keepAlive).map(item => item.name as string) const keepAlivePages = $router.getRoutes().filter(item => item.meta.keepAlive).map(item => item.name as string)
const progressShow = ref(false)
const progressPercentage = ref(0)
onBeforeMount(() => { onBeforeMount(() => {
os.value = process.platform os.value = process.platform
sendToMain(GET_PICBEDS) sendToMain(GET_PICBEDS)
@@ -298,6 +336,10 @@ onBeforeMount(() => {
ipcRenderer.on(SHOW_MAIN_PAGE_DONATION, () => { ipcRenderer.on(SHOW_MAIN_PAGE_DONATION, () => {
visible.value = true visible.value = true
}) })
ipcRenderer.on('updateProgress', (_event: IpcRendererEvent, data: { progress: number}) => {
progressShow.value = data.progress !== 100 && data.progress !== 0
progressPercentage.value = data.progress
})
}) })
watch(() => choosedPicBedForQRCode, (val) => { watch(() => choosedPicBedForQRCode, (val) => {
@@ -393,6 +435,9 @@ onBeforeRouteUpdate(async (to) => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
ipcRenderer.removeListener(GET_PICBEDS, getPicBeds) ipcRenderer.removeListener(GET_PICBEDS, getPicBeds)
ipcRenderer.removeAllListeners(SHOW_MAIN_PAGE_QRCODE)
ipcRenderer.removeAllListeners(SHOW_MAIN_PAGE_DONATION)
ipcRenderer.removeAllListeners('updateProgress')
}) })
</script> </script>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

File diff suppressed because it is too large Load Diff
+323 -269
View File
@@ -70,7 +70,14 @@
style="width: 25px; height: 25px;" style="width: 25px; height: 25px;"
> >
</template> </template>
{{ item.alias }} <el-tooltip
effect="light"
:content="item.alias"
placement="top"
:disabled="isNeedToShorten(item.alias)"
>
{{ isNeedToShorten(item.alias) ? safeSliceF(item.alias, 17) + '...' : item.alias }}
</el-tooltip>
</el-button> </el-button>
</template> </template>
</el-popover> </el-popover>
@@ -251,103 +258,123 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// Vue
import { reactive, ref, onMounted, computed } from 'vue' import { reactive, ref, onMounted, computed } from 'vue'
//
import { supportedPicBedList } from '../utils/constants' import { supportedPicBedList } from '../utils/constants'
// Element Plus
import { Delete, Edit, Pointer, InfoFilled } from '@element-plus/icons-vue' import { Delete, Edit, Pointer, InfoFilled } from '@element-plus/icons-vue'
// Element Plus
import { ElMessage, ElNotification } from 'element-plus' import { ElMessage, ElNotification } from 'element-plus'
//
import { getConfig, saveConfig, removeConfig } from '../utils/dataSender' import { getConfig, saveConfig, removeConfig } from '../utils/dataSender'
// Electron
import { shell } from 'electron' import { shell } from 'electron'
// Vue Router
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
//
import { useManageStore } from '../store/manageStore' import { useManageStore } from '../store/manageStore'
//
import { formObjToTableData, svg } from '../utils/common' import { formObjToTableData, svg } from '../utils/common'
//
import { getConfig as getPicBedsConfig } from '@/utils/dataSender' import { getConfig as getPicBedsConfig } from '@/utils/dataSender'
//
import { formatEndpoint } from '~/main/manage/utils/common' import { formatEndpoint } from '~/main/manage/utils/common'
import { isNeedToShorten, safeSliceF } from '#/utils/common'
//
import { T as $T } from '@/i18n' import { T as $T } from '@/i18n'
const manageStore = useManageStore()
const router = useRouter()
const isLoading = ref(false)
const activeName = ref('login') const activeName = ref('login')
const configResult:IStringKeyMap = reactive({}) const configResult:IStringKeyMap = reactive({})
const existingConfiguration = reactive({} as IStringKeyMap) const existingConfiguration = reactive({} as IStringKeyMap)
const dataForTable = reactive([] as any[]) const dataForTable = reactive([] as any[])
const allConfigAliasMap = reactive({} as IStringKeyMap) const allConfigAliasMap = reactive({} as IStringKeyMap)
const router = useRouter() const currentAliasList = reactive([] as string[])
const manageStore = useManageStore() const rules = ruleMap(supportedPicBedList)
const isLoading = ref(false)
const sortedAllConfigAliasMap = computed(() => { const sortedAllConfigAliasMap = computed(() => {
const sorted = Object.values(allConfigAliasMap).sort((a, b) => { return Object.values(allConfigAliasMap).sort((a, b) => {
return a.picBedName.localeCompare(b.picBedName) return a.picBedName.localeCompare(b.picBedName)
}) })
return sorted
}) })
const currentAliasList = reactive([] as string[]) const importedNewConfig: IStringKeyMap = {}
const ruleMap = (options: IStringKeyMap) => { function ruleMap (options: IStringKeyMap) {
const rule:any = {} const rule:any = {}
Object.keys(options).forEach((key) => { Object.keys(options).forEach((key) => {
const item = options[key].options const item = options[key].options
item.forEach((option: string) => { item.forEach((option: string) => {
const keyName = key + '.' + option const configOptions = options[key].configOptions[option]
if (options[key].configOptions[option].rule) { const keyName = `${key}.${option}`
rule[keyName] = options[key].configOptions[option].rule if (configOptions.rule) {
rule[keyName] = configOptions.rule
} }
if (options[key].configOptions[option].default) { if (configOptions.default) {
configResult[keyName] = options[key].configOptions[option].default configResult[keyName] = configOptions.default
} }
}) })
}) })
return rule return rule
} }
const rules = ruleMap(supportedPicBedList) function getDataForTable () {
const getDataForTable = () => {
for (const key in existingConfiguration) { for (const key in existingConfiguration) {
const obj = {} as IStringKeyMap dataForTable.push({ ...existingConfiguration[key] as IStringKeyMap })
for (const option in existingConfiguration[key]) {
obj[option] = existingConfiguration[key][option]
}
dataForTable.push(obj)
} }
} }
const getExistingConfig = async (name:string) => { async function getExistingConfig (name:string) {
if (name === 'login') { if (name === 'login') {
getAllConfigAliasArray() getAllConfigAliasArray()
return return
} }
currentAliasList.length = 0 currentAliasList.length = 0
const result = await getConfig<any>('picBed') const result = await getConfig<any>('picBed')
if (!result) {
existingConfiguration[name] = { fail: '暂无配置' }
}
for (const key in existingConfiguration) { for (const key in existingConfiguration) {
delete existingConfiguration[key] delete existingConfiguration[key]
} }
for (const key in result) { if (!result || typeof result !== 'object' || Object.keys(result).length === 0) {
if (result[key].picBedName === name) { existingConfiguration[name] = { fail: '暂无配置' }
existingConfiguration[key] = result[key] } else {
currentAliasList.push(key) for (const key in result) {
if (result[key].picBedName === name) {
existingConfiguration[key] = result[key]
currentAliasList.push(key)
}
} }
} }
dataForTable.length = 0 dataForTable.length = 0
getDataForTable() getDataForTable()
handleConfigImport(currentAliasList[0]) handleConfigImport(currentAliasList[0])
} }
const getAliasList = () => { function getAliasList () {
const aliasList = [] as string[] return Object.values(existingConfiguration).map(item => item.alias)
for (const key in existingConfiguration) {
aliasList.push(existingConfiguration[key].alias)
}
return aliasList
} }
const handleConfigChange = async (name: string) => { async function handleConfigChange (name: string) {
const aliasList = getAliasList() const aliasList = getAliasList()
const allKeys = Object.keys(supportedPicBedList[name].configOptions) const allKeys = Object.keys(supportedPicBedList[name].configOptions)
const resultMap:IStringKeyMap = {} const resultMap:IStringKeyMap = {}
const reg = /^[\u4e00-\u9fa5_a-zA-Z0-9-]+$/ const reg = /^[\p{Unified_Ideograph}_a-zA-Z0-9-]+$/u
for (const key of allKeys) { for (const key of allKeys) {
const resultKey = name + '.' + key const resultKey = name + '.' + key
if (supportedPicBedList[name].configOptions[key].required) { if (supportedPicBedList[name].configOptions[key].required) {
@@ -442,24 +469,31 @@ const handleConfigChange = async (name: string) => {
} }
const handleConfigReset = (name: string) => { const handleConfigReset = (name: string) => {
let keys = Object.keys(configResult) const keys = Object.keys(configResult).filter((key) => key.startsWith(name))
keys = keys.filter((key) => key.startsWith(name))
keys.forEach((key) => { keys.forEach((key) => {
configResult[key] = supportedPicBedList[name].configOptions[key.split('.')[1]].default || '' const optionKey = key.split('.')[1]
const configOption = supportedPicBedList[name]?.configOptions?.[optionKey]
if (configOption) {
configResult[key] = configOption.default || ''
}
}) })
} }
const handleConfigRemove = (name: string) => { const handleConfigRemove = (name: string) => {
const commonNoticeConfig = {
title: $T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_NAME'),
duration: 2000,
customClass: 'notification',
offset: 100
}
try { try {
removeConfig('picBed', name) removeConfig('picBed', name)
ElNotification( ElNotification(
{ {
title: $T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_NAME'), ...commonNoticeConfig,
message: `${$T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_MESSAGE_C')}${name}`, message: `${$T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_MESSAGE_C')}${name}`,
type: 'success', type: 'success',
duration: 2000,
customClass: 'notification',
offset: 100,
position: 'bottom-right' position: 'bottom-right'
} }
) )
@@ -468,12 +502,9 @@ const handleConfigRemove = (name: string) => {
} catch (error) { } catch (error) {
ElNotification( ElNotification(
{ {
title: $T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_NAME'), ...commonNoticeConfig,
message: `${$T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_MESSAGE_D')}${name}${$T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_MESSAGE_E')}`, message: `${$T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_MESSAGE_D')}${name}${$T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_MESSAGE_E')}`,
type: 'error', type: 'error',
duration: 2000,
customClass: 'notification',
offset: 100,
position: 'bottom-right' position: 'bottom-right'
} }
) )
@@ -485,17 +516,13 @@ const getAllConfigAliasArray = async () => {
for (const key in allConfigAliasMap) { for (const key in allConfigAliasMap) {
delete allConfigAliasMap[key] delete allConfigAliasMap[key]
} }
if (!result) { if (!result) return
return Object.entries(result).forEach(([, value]: [string, any], index) => {
} allConfigAliasMap[index] = {
let i = 0 alias: value.alias,
Object.keys(result).forEach((key) => { picBedName: value.picBedName,
allConfigAliasMap[i] = { config: value
alias: result[key].alias,
picBedName: result[key].picBedName,
config: result[key]
} }
i++
}) })
} }
@@ -524,244 +551,271 @@ const handleConfigClick = async (item: any) => {
function handleConfigImport (alias: string) { function handleConfigImport (alias: string) {
const selectedConfig = existingConfiguration[alias] const selectedConfig = existingConfiguration[alias]
if (selectedConfig) { if (!selectedConfig) return
supportedPicBedList[selectedConfig.picBedName].options.forEach((option: any) => {
if (selectedConfig[option] !== undefined) { supportedPicBedList[selectedConfig.picBedName].options.forEach((option: any) => {
configResult[selectedConfig.picBedName + '.' + option] = selectedConfig[option] if (selectedConfig[option] !== undefined) {
} configResult[selectedConfig.picBedName + '.' + option] = selectedConfig[option]
if (typeof selectedConfig[option] === 'boolean') { }
configResult[selectedConfig.picBedName + '.' + option] = selectedConfig[option] })
}
})
}
} }
async function getCurrentConfigList () { async function getCurrentConfigList () {
const configList = await getPicBedsConfig<any>('uploader') ?? {} const configList = await getPicBedsConfig<any>('uploader') ?? {}
const pbList = ['aliyun', 'tcyun', 'upyun', 'qiniu', 'smms', 'qiniu', 'github', 'webdavplist', 'aws-s3', 'imgur'] const pbList = ['aliyun', 'aws-s3', 'github', 'imgur', 'local', 'qiniu', 'sftpplist', 'smms', 'tcyun', 'upyun', 'webdavplist']
const filteredConfigList = pbList.map((pb) => {
const filteredConfigList = pbList.flatMap((pb) => {
const config = configList[pb] const config = configList[pb]
if (config && config.configList.length > 0) { return config?.configList?.length ? config.configList.map((item: any) => ({ ...item, type: pb })) : []
config.configList.forEach((item: any) => { })
item.type = pb
})
return config
} else {
return null
}
}).filter((config) => config && config.configList.length > 0)
await getAllConfigAliasArray() await getAllConfigAliasArray()
const promises: Promise<any>[] = [] const autoImport = await getPicBedsConfig<boolean>('settings.autoImport') || false
for (const config of filteredConfigList.flatMap((config) => config.configList)) { if (!autoImport) return
const pb = config.type const autoImportPicBed = initArray(await getPicBedsConfig<string | string[]>('settings.autoImportPicBed') || '', [])
promises.push(transUpToManage(config, pb)) await Promise.all(filteredConfigList.flatMap((config) => transUpToManage(config, config.type, autoImportPicBed)))
if (Object.keys(importedNewConfig).length > 0) {
const oldConfig = await getConfig<any>('picBed')
const newConfig = { ...oldConfig, ...importedNewConfig }
saveConfig('picBed', newConfig)
await manageStore.refreshConfig()
} }
await Promise.all(promises)
} }
function isImported (alias: string) { function isImported (alias: string) {
for (const key in allConfigAliasMap) { return Object.values(allConfigAliasMap).some((item) => item.alias === alias)
if (allConfigAliasMap[key].alias === alias) {
return true
}
}
return false
} }
async function transUpToManage (config: IUploaderConfigListItem, picBedName: string) { function initArray (arrayT: string | string[], defaultValue: string[]) {
const autoImport = await getConfig<boolean>('settings.autoImport') || false if (!Array.isArray(arrayT)) {
if (!autoImport) { arrayT = arrayT ? [arrayT] : defaultValue
return }
return arrayT
}
async function transUpToManage (config: IUploaderConfigListItem, picBedName: string, autoImportPicBed: string[]) {
const alias = `${picBedName === 'webdavplist'
? 'webdav'
: picBedName === 'sftpplist'
? 'sftp'
: picBedName === 'aws-s3'
? 's3plist'
: picBedName}-${config._configName ?? 'Default'}-imp`
if (!autoImportPicBed.includes(picBedName) || isImported(alias)) return
const commonConfig = {
alias,
picBedName,
paging: true
} }
let alias: string = ''
const resultMap: IStringKeyMap = {} const resultMap: IStringKeyMap = {}
switch (picBedName) { switch (picBedName) {
case 'smms': case 'smms':
alias = `smms-${config._configName ?? 'Default'}-imp` if (!config.token) return
if (!config.token || isImported(alias)) { Object.assign(resultMap, {
return ...commonConfig,
} token: config.token
resultMap.alias = alias })
resultMap.picBedName = 'smms'
resultMap.token = config.token
resultMap.paging = true
saveConfig(`picBed.${resultMap.alias}`, resultMap)
break break
case 'aliyun': case 'aliyun':
if (!config.accessKeyId || !config.accessKeySecret) { if (!config.accessKeyId || !config.accessKeySecret) return
return Object.assign(resultMap, {
} ...commonConfig,
resultMap.alias = `aliyun-${config._configName ?? 'Default'}-imp` accessKeyId: config.accessKeyId,
resultMap.picBedName = 'aliyun' accessKeySecret: config.accessKeySecret,
resultMap.accessKeyId = config.accessKeyId bucketName: '',
resultMap.accessKeySecret = config.accessKeySecret baseDir: '/',
resultMap.bucketName = '' itemsPerPage: 50,
resultMap.baseDir = '/' isAutoCustomUrl: !config.customUrl,
resultMap.paging = true transformedConfig: JSON.stringify(config.customUrl
resultMap.itemsPerPage = 50
resultMap.isAutoCustomUrl = !config.customUrl
resultMap.transformedConfig = JSON.stringify(config.customUrl
? {
[config.bucket]: {
customUrl: config.customUrl
}
}
: {})
resultMap.paging = true
saveConfig(`picBed.${resultMap.alias}`, resultMap)
break
case 'qiniu':
alias = `qiniu-${config._configName ?? 'Default'}-imp`
if (!config.accessKey || !config.secretKey || isImported(alias)) {
return
}
resultMap.alias = alias
resultMap.picBedName = 'qiniu'
resultMap.accessKey = config.accessKey
resultMap.secretKey = config.secretKey
resultMap.bucketName = ''
resultMap.baseDir = '/'
resultMap.isAutoCustomUrl = false
resultMap.transformedConfig = JSON.stringify({ [config.bucket]: config.url })
resultMap.paging = true
resultMap.itemsPerPage = 50
saveConfig(`picBed.${resultMap.alias}`, resultMap)
break
case 'tcyun':
alias = `tcyun-${config._configName ?? 'Default'}-imp`
if (!config.secretId || !config.secretKey || config.version === 'v4' || isImported(alias)) {
return
}
resultMap.alias = alias
resultMap.picBedName = 'tcyun'
resultMap.secretId = config.secretId
resultMap.secretKey = config.secretKey
resultMap.bucketName = ''
resultMap.baseDir = '/'
resultMap.appId = config.appId
resultMap.isAutoCustomUrl = !config.customUrl
resultMap.transformedConfig = JSON.stringify(config.customUrl
? {
[config.bucket]: {
customUrl: config.customUrl
}
}
: {})
resultMap.paging = true
resultMap.itemsPerPage = 50
saveConfig(`picBed.${resultMap.alias}`, resultMap)
break
case 'github':
alias = `github-${config._configName ?? 'Default'}-imp`
if (!config.token || isImported(alias)) {
return
}
resultMap.alias = alias
resultMap.picBedName = 'github'
resultMap.token = config.token
resultMap.githubUsername = config.repo.split('/')[0]
resultMap.customUrl = ''
resultMap.proxy = ''
resultMap.paging = true
resultMap.itemsPerPage = 50
saveConfig(`picBed.${resultMap.alias}`, resultMap)
break
case 'upyun':
alias = `upyun-${config._configName ?? 'Default'}-imp`
if (!config.operator || !config.password || isImported(alias)) {
return
}
resultMap.alias = alias
resultMap.picBedName = 'upyun'
resultMap.operator = config.operator
resultMap.password = config.password
resultMap.bucketName = config.bucket
resultMap.baseDir = '/'
resultMap.customUrl = config.url
resultMap.transformedConfig = JSON.stringify({
[config.bucket]: {
customUrl: config.url,
baseDir: '/',
area: '',
operator: config.operator,
password: config.password
}
})
resultMap.paging = true
resultMap.itemsPerPage = 50
saveConfig(`picBed.${resultMap.alias}`, resultMap)
break
case 'webdavplist':
alias = `webdav-${config._configName ?? 'Default'}-imp`
if (!config.host || isImported(alias)) {
return
}
resultMap.alias = alias
resultMap.endpoint = formatEndpoint(config.host, config.sslEnabled)
resultMap.picBedName = 'webdavplist'
resultMap.username = config.username
resultMap.password = config.password
resultMap.bucketName = 'webdav'
resultMap.baseDir = config.path || '/'
resultMap.customUrl = config.customUrl || ''
resultMap.sslEnabled = !!config.sslEnabled
resultMap.proxy = ''
resultMap.transformedConfig = JSON.stringify({
webdav: {
operator: '',
password: config.password,
baseDir: config.path || '/',
customUrl: config.customUrl || '',
area: ''
}
})
saveConfig(`picBed.${resultMap.alias}`, resultMap)
break
case 'aws-s3':
alias = `aws-s3-${config._configName ?? 'Default'}-imp`
if (!config.accessKeyID || !config.secretAccessKey || isImported(alias)) {
return
}
resultMap.alias = alias
resultMap.picBedName = 's3plist'
resultMap.accessKeyId = config.accessKeyID
resultMap.secretAccessKey = config.secretAccessKey
resultMap.endpoint = config.endpoint || ''
resultMap.baseDir = '/'
resultMap.bucketName = ''
resultMap.paging = true
resultMap.itemsPerPage = 50
resultMap.proxy = ''
resultMap.sslEnabled = config.endpoint ? config.endpoint.startsWith('https') : false
resultMap.aclForUpload = 'public-read'
resultMap.s3ForcePathStyle = config.pathStyleAccess
resultMap.transformedConfig = JSON.stringify(
config.urlPrefix
? { ? {
[config.bucketName]: { [config.bucket]: {
customUrl: config.urlPrefix customUrl: config.customUrl
} }
} }
: {} : {})
) })
saveConfig(`picBed.${resultMap.alias}`, resultMap) break
case 'qiniu':
if (!config.accessKey || !config.secretKey) return
Object.assign(resultMap, {
...commonConfig,
accessKey: config.accessKey,
secretKey: config.secretKey,
bucketName: '',
baseDir: '/',
isAutoCustomUrl: false,
transformedConfig: JSON.stringify({ [config.bucket]: config.url }),
itemsPerPage: 50
})
break
case 'tcyun':
if (!config.secretId || !config.secretKey || config.version === 'v4') return
Object.assign(resultMap, {
...commonConfig,
secretId: config.secretId,
secretKey: config.secretKey,
bucketName: '',
baseDir: '/',
appId: config.appId,
isAutoCustomUrl: !config.customUrl,
transformedConfig: JSON.stringify(config.customUrl
? {
[config.bucket]: {
customUrl: config.customUrl
}
}
: {}),
itemsPerPage: 50
})
break
case 'github':
if (!config.token) return
Object.assign(resultMap, {
...commonConfig,
token: config.token,
githubUsername: config.repo.split('/')[0],
customUrl: '',
proxy: '',
itemsPerPage: 50
})
break
case 'upyun':
if (!config.operator || !config.password) return
Object.assign(resultMap, {
...commonConfig,
operator: config.operator,
password: config.password,
bucketName: config.bucket,
baseDir: '/',
customUrl: config.url,
transformedConfig: JSON.stringify({
[config.bucket]: {
customUrl: config.url,
baseDir: '/',
area: '',
operator: config.operator,
password: config.password
}
}),
itemsPerPage: 50
})
break
case 'webdavplist':
if (!config.host) return
Object.assign(resultMap, {
...commonConfig,
endpoint: formatEndpoint(config.host, config.sslEnabled),
username: config.username,
password: config.password,
bucketName: 'webdav',
baseDir: config.path || '/',
webPath: config.webpath || '',
customUrl: config.customUrl || '',
sslEnabled: !!config.sslEnabled,
proxy: '',
transformedConfig: JSON.stringify({
webdav: {
operator: '',
password: config.password,
baseDir: config.path || '/',
customUrl: config.customUrl || '',
area: ''
}
})
})
delete resultMap.paging
break
case 'local':
if (!config.path) return
Object.assign(resultMap, {
...commonConfig,
baseDir: config.path,
webPath: config.webpath || '',
customUrl: config.customUrl || '',
transformedConfig: JSON.stringify({
local: {
customUrl: config.customUrl || '',
baseDir: config.path,
webPath: config.webpath || ''
}
})
})
delete resultMap.paging
break
case 'sftpplist':
if (!config.host) return
Object.assign(resultMap, {
...commonConfig,
picBedName: 'sftp',
host: config.host,
port: config.port || 22,
username: config.username,
password: config.password,
privateKey: config.privateKey,
passphrase: config.passphrase,
baseDir: config.uploadPath || '/',
webPath: config.webPath || '',
customUrl: config.customUrl || '',
fileMode: config.fileMode || '0664',
dirMode: config.dirMode || '0775',
transformedConfig: JSON.stringify({
sftp: {
host: config.host,
port: config.port || 22,
username: config.username,
password: config.password,
privateKey: config.privateKey,
passphrase: config.passphrase,
baseDir: config.uploadPath || '/',
webPath: config.webPath || '',
customUrl: config.customUrl || '',
fileMode: config.fileMode || '0664',
dirMode: config.dirMode || '0775'
}
})
})
delete resultMap.paging
break
case 'aws-s3':
if (!config.accessKeyID || !config.secretAccessKey) return
Object.assign(resultMap, {
...commonConfig,
picBedName: 's3plist',
accessKeyId: config.accessKeyID,
secretAccessKey: config.secretAccessKey,
endpoint: config.endpoint || '',
bucketName: '',
baseDir: '/',
itemsPerPage: 50,
proxy: '',
sslEnabled: config.endpoint ? config.endpoint.startsWith('https') : false,
aclForUpload: 'public-read',
s3ForcePathStyle: config.pathStyleAccess,
dogeCloudSupport: false,
transformedConfig: JSON.stringify(
config.urlPrefix
? {
[config.bucketName]: {
customUrl: config.urlPrefix
}
}
: {}
)
})
break break
case 'imgur': case 'imgur':
alias = `imgur-${config._configName ?? 'Default'}-imp` if (!config.username || !config.accessToken) return
if (!config.username || !config.accessToken || isImported(alias)) { Object.assign(resultMap, {
return ...commonConfig,
} username: config.username,
resultMap.alias = alias accessToken: config.accessToken,
resultMap.picBedName = 'imgur' proxy: ''
resultMap.imgurUserName = config.username })
resultMap.accessToken = config.accessToken delete resultMap.paging
resultMap.proxy = config.proxy || ''
saveConfig(`picBed.${resultMap.alias}`, resultMap)
break break
default: default:
return return
} }
manageStore.refreshConfig() importedNewConfig[alias] = resultMap
} }
onMounted(async () => { onMounted(async () => {
+132 -103
View File
@@ -127,7 +127,7 @@
<el-icon <el-icon
class="layout__menu__setting__item__icon" class="layout__menu__setting__item__icon"
> >
<Setting /> <Tools />
</el-icon> </el-icon>
{{ $T('MANAGE_MAIN_PAGE_SETTING') }} {{ $T('MANAGE_MAIN_PAGE_SETTING') }}
</span> </span>
@@ -273,31 +273,56 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// Vue
import { ref, reactive, computed, onBeforeMount, watch } from 'vue' import { ref, reactive, computed, onBeforeMount, watch } from 'vue'
// Electron
import { shell } from 'electron' import { shell } from 'electron'
//
import { supportedPicBedList } from '../utils/constants' import { supportedPicBedList } from '../utils/constants'
import { CirclePlus, SuccessFilled, Folder, Switch, Setting, ChromeFilled, HomeFilled, FolderOpened } from '@element-plus/icons-vue'
// Element Plus
import { CirclePlus, SuccessFilled, Folder, Switch, Tools, ChromeFilled, HomeFilled, FolderOpened } from '@element-plus/icons-vue'
// Vue Router
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
// Element Plus
import { ElNotification } from 'element-plus' import { ElNotification } from 'element-plus'
//
import { invokeToMain } from '../utils/dataSender' import { invokeToMain } from '../utils/dataSender'
//
import { newBucketConfig } from '../utils/newBucketConfig' import { newBucketConfig } from '../utils/newBucketConfig'
//
import { useManageStore } from '../store/manageStore' import { useManageStore } from '../store/manageStore'
//
import { T as $T } from '@/i18n' import { T as $T } from '@/i18n'
import path from 'path'
const manageStore = useManageStore() as any const manageStore = useManageStore() as any
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const currentAlias = ref(route.query.alias as string) const currentAlias = ref(route.query.alias as string)
const currentPicBedName = ref(route.query.picBedName as string) const currentPicBedName = ref(route.query.picBedName as string)
let allPicBedConfigure = JSON.parse(route.query.allPicBedConfigure as string) let allPicBedConfigure = JSON.parse(route.query.allPicBedConfigure as string)
let currentPagePicBedConfig = reactive(JSON.parse(route.query.config as string)) let currentPagePicBedConfig = reactive(JSON.parse(route.query.config as string))
const picBedSwitchDialogVisible = ref(false)
const newBucketConfigResult: IStringKeyMap = reactive({}) const newBucketConfigResult: IStringKeyMap = reactive({})
const bucketList = ref({} as IStringKeyMap) const bucketList = ref({} as IStringKeyMap)
const currentSelectedBucket = ref('') const currentSelectedBucket = ref('')
const isLoadingBucketList = ref(false)
const bucketNameList = ref([] as string[]) const bucketNameList = ref([] as string[])
const isLoadingBucketList = ref(false)
const nweBucketDrawerVisible = ref(false)
const picBedSwitchDialogVisible = ref(false)
watch(route, async (newRoute) => { watch(route, async (newRoute) => {
if (newRoute.fullPath.split('?')[0] === '/main-page/manage-main-page') { if (newRoute.fullPath.split('?')[0] === '/main-page/manage-main-page') {
currentAlias.value = newRoute.query.alias as string currentAlias.value = newRoute.query.alias as string
@@ -311,64 +336,80 @@ watch(route, async (newRoute) => {
const getCurrentActiveBucket = computed(() => bucketNameList.value.length === 0 ? '' : bucketNameList.value[0]) const getCurrentActiveBucket = computed(() => bucketNameList.value.length === 0 ? '' : bucketNameList.value[0])
const urlMap : IStringKeyMap = { const urlMap : IStringKeyMap = {
smms: 'https://smms.app', aliyun: 'https://oss.console.aliyun.com',
github: 'https://github.com', github: 'https://github.com',
imgur: 'https://imgur.com', imgur: 'https://imgur.com',
aliyun: 'https://oss.console.aliyun.com', local: 'https://piclist.cn',
qiniu: 'https://portal.qiniu.com', qiniu: 'https://portal.qiniu.com',
s3plist: 'https://aws.amazon.com/cn/s3/',
sftp: 'https://github.com/imba97/picgo-plugin-sftp-uploader',
smms: 'https://smms.app',
tcyun: 'https://console.cloud.tencent.com/cos', tcyun: 'https://console.cloud.tencent.com/cos',
upyun: 'https://console.upyun.com', upyun: 'https://console.upyun.com',
s3plist: 'https://aws.amazon.com/cn/s3/',
webdavplist: 'https://baike.baidu.com/item/WebDAV/4610909' webdavplist: 'https://baike.baidu.com/item/WebDAV/4610909'
} }
const openPicBedUrl = () => shell.openExternal(urlMap[currentPagePicBedConfig.picBedName]) const showNewIconList = ['aliyun', 'qiniu', 'tcyun']
const ruleMap = (options: IStringKeyMap) => { const bucketT = $T('MANAGE_MAIN_PAGE_BUCKET')
const rule: IStringKeyMap = {} const galleryT = $T('MANAGE_MAIN_PAGE_GALLERY')
Object.keys(options).forEach((key) => { const repositoryT = $T('MANAGE_MAIN_PAGE_REPOSITORY')
const item = options[key].options
item.forEach((option: string) => { const menuTitleMap:IStringKeyMap = {
const keyName = `${key}.${option}` aliyun: bucketT,
if (options[key].configOptions[option].rule) { qiniu: bucketT,
rule[keyName] = options[key].configOptions[option].rule tcyun: bucketT,
} upyun: bucketT,
if (options[key].configOptions[option].default) { s3plist: bucketT,
newBucketConfigResult[keyName] = options[key].configOptions[option].default sftp: '',
} smms: galleryT,
}) imgur: galleryT,
}) github: repositoryT,
return rule webdavplist: '',
local: ''
} }
const rules = ruleMap(newBucketConfig) const rules = ruleMap(newBucketConfig)
const openNewBucketDrawer = () => { const openPicBedUrl = () => shell.openExternal(urlMap[currentPagePicBedConfig.picBedName])
function ruleMap (options: IStringKeyMap) {
return Object.keys(options).reduce((result, key) => {
options[key].options.forEach((option: string) => {
const keyName = `${key}.${option}`
const configOption = options[key].configOptions[option]
if (configOption.rule) {
result[keyName] = configOption.rule
}
if (configOption.default) {
newBucketConfigResult[keyName] = configOption.default
}
})
return result
}, {} as IStringKeyMap)
}
function openNewBucketDrawer () {
nweBucketDrawerVisible.value = true nweBucketDrawerVisible.value = true
} }
const createNewBucket = (picBedName: string) => { function createNewBucket (picBedName: string) {
const allKeys = Object.keys(newBucketConfig[picBedName].configOptions) const configOptions = newBucketConfig[picBedName].configOptions
const resultMap: IStringKeyMap = {} const resultMap: IStringKeyMap = Object.keys(configOptions).reduce((result, key) => {
for (const key of allKeys) {
const resultKey = `${picBedName}.${key}` const resultKey = `${picBedName}.${key}`
if (newBucketConfig[picBedName].configOptions[key].default !== undefined && newBucketConfigResult[resultKey] === '') { const defaultValue = configOptions[key].default
resultMap[key] = newBucketConfig[picBedName].configOptions[key].default const resultValue = newBucketConfigResult[resultKey]
} else if (newBucketConfigResult[resultKey] === undefined) {
if (newBucketConfig[picBedName].configOptions[key].default !== undefined) { result[key] = resultValue === '' && defaultValue !== undefined
resultMap[key] = newBucketConfig[picBedName].configOptions[key].default ? defaultValue
} else { : resultValue === undefined ? defaultValue ?? '' : resultValue
resultMap[key] = ''
} return result
} else { }, {} as IStringKeyMap)
resultMap[key] = newBucketConfigResult[resultKey]
}
}
if (currentPicBedName.value === 'tcyun') { if (currentPicBedName.value === 'tcyun') {
resultMap.BucketName = resultMap.BucketName + '-' + currentPagePicBedConfig.appId resultMap.BucketName = `${resultMap.BucketName}-${currentPagePicBedConfig.appId}`
} }
const res = invokeToMain('createBucket', currentAlias, resultMap) invokeToMain('createBucket', currentAlias, resultMap).then((result: any) => {
res.then((result: any) => {
if (result) { if (result) {
ElNotification({ ElNotification({
title: $T('MANAGE_MAIN_PAGE_TIPS'), title: $T('MANAGE_MAIN_PAGE_TIPS'),
@@ -389,12 +430,14 @@ const createNewBucket = (picBedName: string) => {
}) })
} }
const getBucketList = async () => { async function getBucketList () {
bucketList.value = {} bucketList.value = {}
bucketNameList.value = [] bucketNameList.value = []
isLoadingBucketList.value = true isLoadingBucketList.value = true
const result = await invokeToMain('getBucketList', currentAlias.value) const result = await invokeToMain('getBucketList', currentAlias.value)
isLoadingBucketList.value = false isLoadingBucketList.value = false
if (result.length > 0) { if (result.length > 0) {
result.forEach((item: any) => { result.forEach((item: any) => {
bucketList.value[item.Name] = item bucketList.value[item.Name] = item
@@ -403,28 +446,34 @@ const getBucketList = async () => {
} }
} }
const handleSelectMenu = (bucketName: string) => { function transPathToUnix (filePath: string | undefined) {
const transformedConfig = JSON.parse(manageStore.config.picBed[currentAlias.value].transformedConfig ?? '{}') if (!filePath) return ''
let prefix = transformedConfig[bucketName]?.baseDir return process.platform === 'win32' ? filePath.split(path.sep).join(path.posix.sep).replace(/^\/+|\/+$/g, '') : filePath.replace(/^\/+|\/+$/g, '')
if (prefix === '' || prefix === undefined) { }
prefix = '/'
function handleSelectMenu (bucketName: string) {
const currentPicBedConfig = manageStore.config.picBed[currentAlias.value]
const transformedConfig = JSON.parse(currentPicBedConfig.transformedConfig ?? '{}')
let prefix = transformedConfig[bucketName]?.baseDir || '/'
const cpicBedName = currentPicBedConfig.picBedName ?? currentPicBedName.value
if (cpicBedName === 'local') {
prefix = `/${transPathToUnix(prefix)}/`
} else { } else {
!prefix.startsWith('/') && (prefix = `/${prefix}`) prefix = prefix.startsWith('/') ? prefix : `/${prefix}`
!prefix.endsWith('/') && (prefix = `${prefix}/`) prefix = prefix.endsWith('/') ? prefix : `${prefix}/`
} }
const customUrl = transformedConfig[bucketName]?.customUrl ?? ''
const picBedName = manageStore.config.picBed[currentAlias.value].picBedName ?? currentPicBedName.value
const alias = currentAlias.value
const cdnUrl = manageStore.config.picBed[currentAlias.value].customUrl
const bucketConfig = bucketList.value[bucketName]
const configMap = { const configMap = {
prefix, prefix,
bucketName, bucketName,
customUrl, customUrl: transformedConfig[bucketName]?.customUrl ?? '',
picBedName, picBedName: cpicBedName,
alias, alias: currentAlias.value,
bucketConfig, bucketConfig: bucketList.value[bucketName],
cdnUrl cdnUrl: currentPicBedConfig.customUrl,
baseDir: prefix,
webPath: currentPicBedConfig.webPath || ''
} }
currentSelectedBucket.value = bucketName currentSelectedBucket.value = bucketName
router.push({ router.push({
@@ -435,60 +484,40 @@ const handleSelectMenu = (bucketName: string) => {
}) })
} }
const nweBucketDrawerVisible = ref(false)
const bucketT = $T('MANAGE_MAIN_PAGE_BUCKET')
const galleryT = $T('MANAGE_MAIN_PAGE_GALLERY')
const repositoryT = $T('MANAGE_MAIN_PAGE_REPOSITORY')
const menuTitleMap:IStringKeyMap = {
aliyun: bucketT,
qiniu: bucketT,
tcyun: bucketT,
upyun: bucketT,
s3plist: bucketT,
smms: galleryT,
imgur: galleryT,
github: repositoryT,
webdavplist: ''
}
const showNewIconList = ['aliyun', 'qiniu', 'tcyun']
function switchPicBed (picBedAlias:string) { function switchPicBed (picBedAlias:string) {
if (picBedAlias === 'main') { if (picBedAlias === 'main') {
router.push({ router.push({
path: '/main-page/manage-login-page' path: '/main-page/manage-login-page'
}) })
return
}
if (route.fullPath.startsWith('/main-page/manage-main-page/manage-bucket-page') || route.fullPath.startsWith('/main-page/manage-main-page/manage-setting-page')
) {
picBedSwitchDialogVisible.value = false
router.push({
path: '/main-page/manage-main-page',
query: {
alias: picBedAlias,
picBedName: allPicBedConfigure[picBedAlias].picBedName,
config: JSON.stringify(allPicBedConfigure[picBedAlias]),
allPicBedConfigure: JSON.stringify(allPicBedConfigure)
}
})
} else { } else {
if (route.fullPath.startsWith('/main-page/manage-main-page/manage-bucket-page') || route.fullPath.startsWith('/main-page/manage-main-page/manage-setting-page') currentAlias.value = picBedAlias
) { currentPicBedName.value = allPicBedConfigure[picBedAlias].picBedName
picBedSwitchDialogVisible.value = false currentPagePicBedConfig = allPicBedConfigure[picBedAlias]
router.push({ picBedSwitchDialogVisible.value = false
path: '/main-page/manage-main-page', currentSelectedBucket.value = ''
query: { getBucketList()
alias: picBedAlias,
picBedName: allPicBedConfigure[picBedAlias].picBedName,
config: JSON.stringify(allPicBedConfigure[picBedAlias]),
allPicBedConfigure: JSON.stringify(allPicBedConfigure)
}
})
} else {
currentAlias.value = picBedAlias
currentPicBedName.value = allPicBedConfigure[picBedAlias].picBedName
currentPagePicBedConfig = allPicBedConfigure[picBedAlias]
picBedSwitchDialogVisible.value = false
currentSelectedBucket.value = ''
getBucketList()
}
} }
} }
const changePicBed = () => { function changePicBed () {
picBedSwitchDialogVisible.value = true picBedSwitchDialogVisible.value = true
} }
const openBucketPageSetting = () => { function openBucketPageSetting () {
router.push({ router.push({
path: '/main-page/manage-main-page/manage-setting-page' path: '/main-page/manage-main-page/manage-setting-page'
}) })
+21 -6
View File
@@ -513,14 +513,29 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// Element Plus
import { InfoFilled, Folder } from '@element-plus/icons-vue' import { InfoFilled, Folder } from '@element-plus/icons-vue'
// Vue
import { ref, reactive, onBeforeMount, watch, onBeforeUnmount } from 'vue' import { ref, reactive, onBeforeMount, watch, onBeforeUnmount } from 'vue'
//
import { getConfig, saveConfig, invokeToMain } from '../utils/dataSender' import { getConfig, saveConfig, invokeToMain } from '../utils/dataSender'
// Element Plus
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
//
import { useManageStore } from '../store/manageStore' import { useManageStore } from '../store/manageStore'
import { fileCacheDbInstance } from '../store/bucketFileDb' import { fileCacheDbInstance } from '../store/bucketFileDb'
//
import { formatFileSize, customRenameFormatTable } from '../utils/common' import { formatFileSize, customRenameFormatTable } from '../utils/common'
//
import { T as $T } from '@/i18n' import { T as $T } from '@/i18n'
//
import { selectDownloadFolder } from '../utils/static' import { selectDownloadFolder } from '../utils/static'
const manageStore = useManageStore() const manageStore = useManageStore()
@@ -597,21 +612,21 @@ async function initData () {
form.timestampRename = config.settings.timestampRename ?? false form.timestampRename = config.settings.timestampRename ?? false
form.randomStringRename = config.settings.randomStringRename ?? false form.randomStringRename = config.settings.randomStringRename ?? false
form.customRename = config.settings.customRename ?? false form.customRename = config.settings.customRename ?? false
customRenameFormat.value = config.settings.customRenameFormat ?? '{filename}'
customPasteFormat.value = config.settings.customPasteFormat ?? '$url'
pasteFormat.value = config.settings.pasteFormat ?? 'markdown'
downloadDir.value = config.settings.downloadDir ?? ''
form.isAutoRefresh = config.settings.isAutoRefresh ?? false form.isAutoRefresh = config.settings.isAutoRefresh ?? false
form.isShowThumbnail = config.settings.isShowThumbnail ?? false form.isShowThumbnail = config.settings.isShowThumbnail ?? false
form.isShowList = config.settings.isShowList ?? false form.isShowList = config.settings.isShowList ?? false
form.isIgnoreCase = config.settings.isIgnoreCase ?? false form.isIgnoreCase = config.settings.isIgnoreCase ?? false
form.isForceCustomUrlHttps = config.settings.isForceCustomUrlHttps ?? true form.isForceCustomUrlHttps = config.settings.isForceCustomUrlHttps ?? true
form.isEncodeUrl = config.settings.isEncodeUrl ?? false form.isEncodeUrl = config.settings.isEncodeUrl ?? false
PreSignedExpire.value = config.settings.PreSignedExpire ?? 14400
maxDownloadFileCount.value = config.settings.maxDownloadFileCount ?? 5
form.isUploadKeepDirStructure = config.settings.isUploadKeepDirStructure ?? true form.isUploadKeepDirStructure = config.settings.isUploadKeepDirStructure ?? true
form.isDownloadFileKeepDirStructure = config.settings.isDownloadKeepDirStructure ?? false form.isDownloadFileKeepDirStructure = config.settings.isDownloadKeepDirStructure ?? false
form.isDownloadFolderKeepDirStructure = config.settings.isDownloadFolderKeepDirStructure ?? true form.isDownloadFolderKeepDirStructure = config.settings.isDownloadFolderKeepDirStructure ?? true
PreSignedExpire.value = config.settings.PreSignedExpire ?? 14400
maxDownloadFileCount.value = config.settings.maxDownloadFileCount ?? 5
customRenameFormat.value = config.settings.customRenameFormat ?? '{filename}'
customPasteFormat.value = config.settings.customPasteFormat ?? '$url'
pasteFormat.value = config.settings.pasteFormat ?? 'markdown'
downloadDir.value = config.settings.downloadDir ?? ''
} }
async function handleDownloadDirClick () { async function handleDownloadDirClick () {
+15 -12
View File
@@ -19,35 +19,38 @@ export interface IFileCache {
* new picbed will add a plist suffix to distinguish from picgo * new picbed will add a plist suffix to distinguish from picgo
*/ */
export class FileCacheDb extends Dexie { export class FileCacheDb extends Dexie {
tcyun: Table<IFileCache, string>
aliyun: Table<IFileCache, string> aliyun: Table<IFileCache, string>
qiniu: Table<IFileCache, string>
github: Table<IFileCache, string> github: Table<IFileCache, string>
smms: Table<IFileCache, string>
upyun: Table<IFileCache, string>
imgur: Table<IFileCache, string> imgur: Table<IFileCache, string>
local: Table<IFileCache, string>
tcyun: Table<IFileCache, string>
qiniu: Table<IFileCache, string>
smms: Table<IFileCache, string>
s3plist: Table<IFileCache, string> s3plist: Table<IFileCache, string>
sftp: Table<IFileCache, string>
upyun: Table<IFileCache, string>
webdavplist: Table<IFileCache, string> webdavplist: Table<IFileCache, string>
localplist: Table<IFileCache, string>
constructor () { constructor () {
super('bucketFileDb') super('bucketFileDb')
const tableNames = ['tcyun', 'aliyun', 'qiniu', 'github', 'smms', 'upyun', 'imgur', 's3plist', 'webdavplist', 'localplist'] const tableNames = ['aliyun', 'github', 'imgur', 'local', 'qiniu', 's3plist', 'sftp', 'smms', 'tcyun', 'upyun', 'webdavplist']
const tableNamesMap = tableNames.reduce((acc, cur) => { const tableNamesMap = tableNames.reduce((acc, cur) => {
acc[cur] = '&key, value' acc[cur] = '&key, value'
return acc return acc
}, {} as IStringKeyMap) }, {} as IStringKeyMap)
this.version(3).stores(tableNamesMap) this.version(5).stores(tableNamesMap)
this.tcyun = this.table('tcyun')
this.aliyun = this.table('aliyun') this.aliyun = this.table('aliyun')
this.qiniu = this.table('qiniu')
this.github = this.table('github') this.github = this.table('github')
this.imgur = this.table('imgur')
this.local = this.table('local')
this.qiniu = this.table('qiniu')
this.tcyun = this.table('tcyun')
this.s3plist = this.table('s3plist')
this.sftp = this.table('sftp')
this.smms = this.table('smms') this.smms = this.table('smms')
this.upyun = this.table('upyun') this.upyun = this.table('upyun')
this.imgur = this.table('imgur')
this.s3plist = this.table('s3plist')
this.webdavplist = this.table('webdavplist') this.webdavplist = this.table('webdavplist')
this.localplist = this.table('localplist')
} }
} }
+32 -20
View File
@@ -1,9 +1,20 @@
// UUID
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
// 路径处理库
import path from 'path' import path from 'path'
// 加密库
import crypto from 'crypto' import crypto from 'crypto'
// 可用图标列表
import { availableIconList } from './icon' import { availableIconList } from './icon'
// 数据发送工具函数
import { getConfig } from './dataSender' import { getConfig } from './dataSender'
import { handleUrlEncode } from '~/universal/utils/common'
// 工具函数
import { handleUrlEncode, safeSliceF, isNeedToShorten } from '~/universal/utils/common'
export function randomStringGenerator (length: number): string { export function randomStringGenerator (length: number): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
@@ -41,15 +52,17 @@ export function renameFileNameWithCustomString (oldName: string, customFormat: s
}, customFormat) + ext }, customFormat) + ext
} }
export function renameFile (typeMap : IStringKeyMap, oldName: string): string { export function renameFile ({ timestampRename, randomStringRename, customRename, customRenameFormat }: IStringKeyMap, oldName = ''): string {
if (typeMap.timestampRename) { switch (true) {
return renameFileNameWithTimestamp(oldName) case timestampRename:
} else if (typeMap.randomStringRename) { return renameFileNameWithTimestamp(oldName)
return renameFileNameWithRandomString(oldName, 20) case randomStringRename:
} else if (typeMap.customRename) { return renameFileNameWithRandomString(oldName, 20)
return renameFileNameWithCustomString(oldName, typeMap.customRenameFormat) case customRename:
return renameFileNameWithCustomString(oldName, customRenameFormat)
default:
return oldName
} }
return oldName
} }
export async function formatLink (url: string, fileName: string, type: string, format?: string) : Promise<string> { export async function formatLink (url: string, fileName: string, type: string, format?: string) : Promise<string> {
@@ -92,7 +105,7 @@ export function formatFileName (fileName: string, length: number = 20) {
let ext = path.extname(fileName) let ext = path.extname(fileName)
ext = ext.length > 5 ? ext.slice(ext.length - 5) : ext ext = ext.length > 5 ? ext.slice(ext.length - 5) : ext
const name = path.basename(fileName, ext) const name = path.basename(fileName, ext)
return name.length > length ? `${name.slice(0, length)}...${ext}` : fileName return isNeedToShorten(fileName, length) ? `${safeSliceF(name, length - 3 - ext.length)}...${ext}` : fileName
} }
export const getExtension = (fileName: string) => path.extname(fileName).slice(1) export const getExtension = (fileName: string) => path.extname(fileName).slice(1)
@@ -123,7 +136,7 @@ export interface IHTTPProxy {
} }
export const formatHttpProxy = (proxy: string | undefined, type: 'object' | 'string'): IHTTPProxy | undefined | string => { export const formatHttpProxy = (proxy: string | undefined, type: 'object' | 'string'): IHTTPProxy | undefined | string => {
if (proxy === undefined || proxy === '') return undefined if (!proxy) return undefined
if (/^https?:\/\//.test(proxy)) { if (/^https?:\/\//.test(proxy)) {
const { protocol, hostname, port } = new URL(proxy) const { protocol, hostname, port } = new URL(proxy)
return type === 'string' return type === 'string'
@@ -133,16 +146,15 @@ export const formatHttpProxy = (proxy: string | undefined, type: 'object' | 'str
port: Number(port), port: Number(port),
protocol: protocol.slice(0, -1) protocol: protocol.slice(0, -1)
} }
} else {
const [host, port] = proxy.split(':')
return type === 'string'
? `http://${host}:${port}`
: {
host,
port: port ? Number(port) : 80,
protocol: 'http'
}
} }
const [host, port] = proxy.split(':')
return type === 'string'
? `http://${host}:${port}`
: {
host,
port: port ? Number(port) : 80,
protocol: 'http'
}
} }
export const svg = ` export const svg = `
+234 -9
View File
@@ -22,7 +22,7 @@ const itemsPerPageRule = [
trigger: 'change' trigger: 'change'
}, },
{ {
validator: (rule: any, value: any, callback: any) => { validator: (_rule: any, value: any, callback: any) => {
if (value < 20 || value > 1000) { if (value < 20 || value > 1000) {
callback(new Error($T('MANAGE_CONSTANT_ITEMS_PAGE_RULE_MESSAGE_C'))) callback(new Error($T('MANAGE_CONSTANT_ITEMS_PAGE_RULE_MESSAGE_C')))
} else { } else {
@@ -40,8 +40,8 @@ const aliasRule = [
trigger: 'blur' trigger: 'blur'
}, },
{ {
validator: (rule: any, value: any, callback: any) => { validator: (_rule: any, value: any, callback: any) => {
const reg = /^[\u4e00-\u9fa5_a-zA-Z0-9-]+$/ const reg = /^[\u4e00-\u9fff_a-zA-Z0-9-]+$/
if (!reg.test(value)) { if (!reg.test(value)) {
callback(new Error($T('MANAGE_CONSTANT_ALIAS_RULE_MESSAGE_B'))) callback(new Error($T('MANAGE_CONSTANT_ALIAS_RULE_MESSAGE_B')))
} else { } else {
@@ -469,7 +469,7 @@ export const supportedPicBedList: IStringKeyMap = {
trigger: 'change' trigger: 'change'
}, },
{ {
validator: (rule: any, value: any, callback: any) => { validator: (_rule: any, value: any, callback: any) => {
if (value) { if (value) {
const customUrlList = value.split(',') const customUrlList = value.split(',')
const customUrlValid = customUrlList.every((customUrl: string) => { const customUrlValid = customUrlList.every((customUrl: string) => {
@@ -642,6 +642,13 @@ export const supportedPicBedList: IStringKeyMap = {
default: '/', default: '/',
tooltip: baseDirTooltip tooltip: baseDirTooltip
}, },
dogeCloudSupport: {
required: false,
description: $T('MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_DESC'),
default: false,
type: 'boolean',
tooltip: $T('MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_TOOLTIP')
},
paging: { paging: {
required: true, required: true,
description: $T('MANAGE_CONSTANT_S3_PAGING_DESC'), description: $T('MANAGE_CONSTANT_S3_PAGING_DESC'),
@@ -659,7 +666,7 @@ export const supportedPicBedList: IStringKeyMap = {
} }
}, },
explain: $T('MANAGE_CONSTANT_S3_EXPLAIN'), explain: $T('MANAGE_CONSTANT_S3_EXPLAIN'),
options: ['alias', 'accessKeyId', 'secretAccessKey', 'endpoint', 'sslEnabled', 's3ForcePathStyle', 'proxy', 'aclForUpload', 'bucketName', 'baseDir', 'paging', 'itemsPerPage'], options: ['alias', 'accessKeyId', 'secretAccessKey', 'endpoint', 'sslEnabled', 's3ForcePathStyle', 'proxy', 'aclForUpload', 'bucketName', 'baseDir', 'dogeCloudSupport', 'paging', 'itemsPerPage'],
refLink: 'https://github.com/wayjam/picgo-plugin-s3', refLink: 'https://github.com/wayjam/picgo-plugin-s3',
referenceText: $T('MANAGE_CONSTANT_S3_REFER_TEXT') referenceText: $T('MANAGE_CONSTANT_S3_REFER_TEXT')
}, },
@@ -712,8 +719,7 @@ export const supportedPicBedList: IStringKeyMap = {
description: $T('MANAGE_CONSTANT_WEBDAV_BASE_DIR_DESC'), description: $T('MANAGE_CONSTANT_WEBDAV_BASE_DIR_DESC'),
placeholder: $T('MANAGE_CONSTANT_WEBDAV_BASE_DIR_PLACEHOLDER'), placeholder: $T('MANAGE_CONSTANT_WEBDAV_BASE_DIR_PLACEHOLDER'),
type: 'string', type: 'string',
default: '/', default: '/'
tooltip: baseDirTooltip
}, },
customUrl: { customUrl: {
required: false, required: false,
@@ -723,7 +729,7 @@ export const supportedPicBedList: IStringKeyMap = {
tooltip: $T('MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_TOOLTIP'), tooltip: $T('MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_TOOLTIP'),
rule: [ rule: [
{ {
validator: (rule: any, value: any, callback: any) => { validator: (_rule: any, value: any, callback: any) => {
if (value) { if (value) {
if (!/^https?:\/\/.+/.test(value)) { if (!/^https?:\/\/.+/.test(value)) {
callback(new Error($T('MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_RULE_MESSAGE'))) callback(new Error($T('MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_RULE_MESSAGE')))
@@ -738,6 +744,14 @@ export const supportedPicBedList: IStringKeyMap = {
} }
] ]
}, },
webPath: {
required: false,
description: $T('MANAGE_CONSTANT_WEBDAV_WEB_PATH'),
placeholder: $T('MANAGE_CONSTANT_WEBDAV_WEB_PATH_PLACEHOLDER'),
type: 'string',
tooltip: $T('MANAGE_CONSTANT_WEBDAV_WEB_PATH_TOOLTIP'),
default: ''
},
proxy: { proxy: {
required: false, required: false,
description: $T('MANAGE_CONSTANT_WEBDAV_PROXY_DESC'), description: $T('MANAGE_CONSTANT_WEBDAV_PROXY_DESC'),
@@ -754,8 +768,219 @@ export const supportedPicBedList: IStringKeyMap = {
} }
}, },
explain: $T('MANAGE_CONSTANT_WEBDAV_EXPLAIN'), explain: $T('MANAGE_CONSTANT_WEBDAV_EXPLAIN'),
options: ['alias', 'endpoint', 'username', 'password', 'bucketName', 'baseDir', 'customUrl', 'proxy', 'sslEnabled'], options: ['alias', 'endpoint', 'username', 'password', 'bucketName', 'baseDir', 'customUrl', 'webPath', 'proxy', 'sslEnabled'],
refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=webdav', refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=webdav',
referenceText: $T('MANAGE_CONSTANT_WEBDAV_REFER_TEXT') referenceText: $T('MANAGE_CONSTANT_WEBDAV_REFER_TEXT')
},
local: {
name: $T('MANAGE_CONSTANT_LOCAL_NAME'),
icon: 'local',
configOptions: {
alias: {
required: true,
description: $T('MANAGE_CONSTANT_LOCAL_ALIAS_DESC'),
placeholder: $T('MANAGE_CONSTANT_LOCAL_ALIAS_PLACEHOLDER'),
type: 'string',
rule: aliasRule,
default: 'local-A',
tooltip: aliasTooltip
},
baseDir: {
required: true,
description: $T('MANAGE_CONSTANT_LOCAL_BASE_DIR_DESC'),
placeholder: $T('MANAGE_CONSTANT_LOCAL_BASE_DIR_PLACEHOLDER'),
type: 'string',
default: '',
rule: [
{
validator: (_rule: any, value: any, callback: any) => {
if (!value) {
callback(new Error($T('MANAGE_CONSTANT_LOCAL_BASE_DIR_RULE_MESSAGE')))
} else {
callback()
}
}
}
]
},
customUrl: {
required: false,
description: $T('MANAGE_CONSTANT_LOCAL_CUSTOM_URL_DESC'),
placeholder: $T('MANAGE_CONSTANT_LOCAL_CUSTOM_URL_PLACEHOLDER'),
type: 'string',
tooltip: $T('MANAGE_CONSTANT_LOCAL_CUSTOM_URL_TOOLTIP'),
rule: [
{
validator: (_rule: any, value: any, callback: any) => {
if (value) {
if (!/^https?:\/\/.+/.test(value)) {
callback(new Error($T('MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_RULE_MESSAGE')))
} else {
callback()
}
} else {
callback()
}
},
trigger: 'change'
}
]
},
bucketName: {
required: true,
description: $T('MANAGE_CONSTANT_LOCAL_BUCKET_DESC'),
placeholder: $T('MANAGE_CONSTANT_LOCAL_BUCKET_PLACEHOLDER'),
type: 'string',
default: 'local',
disabled: true,
tooltip: $T('MANAGE_CONSTANT_LOCAL_BUCKET_TOOLTIP')
},
webPath: {
required: false,
description: $T('MANAGE_CONSTANT_LOCAL_WEB_PATH'),
placeholder: $T('MANAGE_CONSTANT_LOCAL_WEB_PATH_PLACEHOLDER'),
type: 'string',
tooltip: $T('MANAGE_CONSTANT_LOCAL_WEB_PATH_TOOLTIP'),
default: ''
}
},
explain: $T('MANAGE_CONSTANT_LOCAL_EXPLAIN'),
options: ['alias', 'baseDir', 'customUrl', 'bucketName', 'webPath'],
refLink: 'https://piclist.cn',
referenceText: $T('MANAGE_CONSTANT_LOCAL_REFER_TEXT')
},
sftp: {
name: $T('MANAGE_CONSTANT_SFTP_NAME'),
icon: 'sftp',
configOptions: {
alias: {
required: true,
description: $T('MANAGE_CONSTANT_SFTP_ALIAS_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_ALIAS_PLACEHOLDER'),
type: 'string',
rule: aliasRule,
default: 'sftp-A',
tooltip: aliasTooltip
},
host: {
required: true,
description: $T('MANAGE_CONSTANT_SFTP_HOST_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_HOST_PLACEHOLDER'),
type: 'string',
rule: defaultBaseRule('host'),
default: ''
},
port: {
required: false,
description: $T('MANAGE_CONSTANT_SFTP_PORT_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_PORT_PLACEHOLDER'),
type: 'number',
default: 22
},
username: {
required: false,
description: $T('MANAGE_CONSTANT_SFTP_USERNAME_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_USERNAME_PLACEHOLDER'),
type: 'string',
default: ''
},
password: {
required: false,
description: $T('MANAGE_CONSTANT_SFTP_PASSWORD_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_PASSWORD_PLACEHOLDER'),
type: 'string',
default: ''
},
privateKey: {
required: false,
description: $T('MANAGE_CONSTANT_SFTP_PRIVATE_KEY_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_PRIVATE_KEY_PLACEHOLDER'),
type: 'string',
default: ''
},
passphrase: {
required: false,
description: $T('MANAGE_CONSTANT_SFTP_PASSPHRASE_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_PASSPHRASE_PLACEHOLDER'),
type: 'string',
default: ''
},
fileMode: {
required: false,
description: $T('MANAGE_CONSTANT_SFTP_FILE_PERMISSIONS_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_FILE_PERMISSIONS_PLACEHOLDER'),
type: 'string',
default: '0664'
},
dirMode: {
required: false,
description: $T('MANAGE_CONSTANT_SFTP_DIR_PERMISSIONS_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_DIR_PERMISSIONS_PLACEHOLDER'),
type: 'string',
default: '0755'
},
baseDir: {
required: true,
description: $T('MANAGE_CONSTANT_SFTP_BASE_DIR_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_BASE_DIR_PLACEHOLDER'),
type: 'string',
default: '',
rule: [
{
validator: (_rule: any, value: any, callback: any) => {
if (!value) {
callback(new Error($T('MANAGE_CONSTANT_SFTP_BASE_DIR_RULE_MESSAGE')))
} else {
callback()
}
}
}
]
},
customUrl: {
required: false,
description: $T('MANAGE_CONSTANT_SFTP_CUSTOM_URL_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_CUSTOM_URL_PLACEHOLDER'),
type: 'string',
tooltip: $T('MANAGE_CONSTANT_SFTP_CUSTOM_URL_TOOLTIP'),
rule: [
{
validator: (_rule: any, value: any, callback: any) => {
if (value) {
if (!/^https?:\/\/.+/.test(value)) {
callback(new Error($T('MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_RULE_MESSAGE')))
} else {
callback()
}
} else {
callback()
}
},
trigger: 'change'
}
]
},
bucketName: {
required: true,
description: $T('MANAGE_CONSTANT_SFTP_BUCKET_DESC'),
placeholder: $T('MANAGE_CONSTANT_SFTP_BUCKET_PLACEHOLDER'),
type: 'string',
default: 'sftp',
disabled: true,
tooltip: $T('MANAGE_CONSTANT_SFTP_BUCKET_TOOLTIP')
},
webPath: {
required: false,
description: $T('MANAGE_CONSTANT_SFTP_WEB_PATH'),
placeholder: $T('MANAGE_CONSTANT_SFTP_WEB_PATH_PLACEHOLDER'),
type: 'string',
tooltip: $T('MANAGE_CONSTANT_SFTP_WEB_PATH_TOOLTIP'),
default: ''
}
},
explain: $T('MANAGE_CONSTANT_SFTP_EXPLAIN'),
options: ['alias', 'host', 'port', 'username', 'password', 'privateKey', 'passphrase', 'fileMode', 'dirMode', 'baseDir', 'customUrl', 'bucketName', 'webPath'],
refLink: 'https://github.com/imba97/picgo-plugin-sftp-uploader',
referenceText: $T('MANAGE_CONSTANT_SFTP_REFER_TEXT')
} }
} }

Some files were not shown because too many files have changed in this diff Show More