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`目录下添加。
## i18n
1.`public/i18n/` 下面创建一种语言的 `yml` 文件,例如 `zh-Hans.yml`。然后参考 `zh-CN.yml` 或者 `en.yml` 编写语言文件。并注意,PicList 会通过语言文件中的 `LANG_DISPLAY_LABEL` 向用户展示该语言的名称。
+21 -2
View File
@@ -27,8 +27,13 @@ PicList所有新功能的添加没有影响到PicGo的原有功能,所以你
- SM.MS
- Imgur
- GitHub
- Webdav
- WebDav
- Aws S3
- 本地路径
- 内置SFTP
- 多吉云
- 华为云 OBS
- Alist
## 4. 能否支持上传视频文件
@@ -46,6 +51,8 @@ PicList本体支持了如下图床:
- `阿里云 OSS`
- `Imgur`
- `Webdav`
- `本地图床`
- `SFTP`
PicList计划整合和优化现有插件,内置更多的常用图床。
@@ -96,4 +103,16 @@ macOS: `~/Library/Application\ Support/picgo/assets/simhei.ttf`
## 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
- Webdav
- Aws S3
- Local path
- Built-in SFTP
- Doge Cloud
- Huawei Cloud OBS
- Alist
## 4. Is it possible to upload video files?
@@ -46,6 +51,8 @@ PicList itself supports the following image hosting platforms:
- Aliyun OSS
- Imgur
- Webdav
- Local path
- SFTP
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
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)
PicList是一款云存储/图床平台管理和文件上传工具,基于PicGo的进行了深度二次开发,保留了PicGo的所有功能的同时,为相册添加了同步云端删除功能,同时增加了完整的云存储管理功能,包括云端目录查看、文件搜索、批量上传下载和删除文件,复制多种格式文件链接和图片/markdown/文本/视频预览等,另外还有更加强大的相册和多项功能新增或优化。
PicList是一款高效的云存储图床平台管理工具,PicGo的基础上经过深度二次开发,不仅完整保留了PicGo的所有功能,还增添了许多新的feature。例如相册支持同步云端删除文件,内置图床额外添加了WebDav、本地图床和SFTP等。PicList同时增加了完整的云存储管理功能,包括云端目录查看、文件搜索、批量上传下载和删除文件,复制多种格式文件链接和图片/markdown/文本/视频预览等,另外还有更加强大的相册和多项功能新增或优化。
## 如何从PicGo迁移
@@ -30,16 +30,17 @@ PicList的内核使用的是原版PicGo-Core基础上修改的[PicList-core](htt
## 特色功能
- 保留了PicGo的所有功能,兼容已有的PicGo插件系统,包括和typora、obsidian等的搭配
- 新增了对webdav上传,imgur账户上传,本地文件夹上传等的支持
- 相册中可同步删除云端图片,同时新增了高级搜索和排序,批量修改URL等功能
- 内置水印添加、图片压缩、图片缩放、图片旋转和图片格式转换等功能,支持自定义配置,且可以通过CLI命令行调用
- 新增配置多端同步功能
- 支持管理所有图床,可以在线进行云端目录查看、文件搜索、批量上传、批量下载、删除文件等
- 保留了PicGo的所有功能,兼容绝大部分已有的PicGo插件,包括和Typora、Obsidian等软件的搭配
- 新增了多个内置图床,如WebDav、本地图床和SFTP等,原内置imgur图床额外支持登录账号上传
- 相册中可同步删除云端图片,支持所有内置图床和多个插件
- 相册新增了高级搜索和排序,批量修改URL等功能
- 内置水印添加、图片压缩、图片缩放、图片旋转和图片格式转换等功能,同时支持高级重命名
- 支持配置同步至Github或Gitee仓库
- 支持管理十余种图床,可以在线进行云端目录查看、文件搜索、批量上传、批量下载、删除文件等
- 支持预览多种格式的文件,包括图片、视频、纯文本文件和markdown文件等,具体支持的格式请参考[支持的文件格式列表](https://github.com/Kuingsmile/PicList/blob/dev/supported_format.md)
- 支持正则表达式的批量云端文件重命名
- 支持启用正则表达式的批量云端文件重命名
- 对于私有存储桶等支持复制预签名链接进行分享
- 支持自动更新,无需每次手动下载,支持多种启动模式选择,还有更多功能细节新增和优化
- 支持软件自动更新,支持多种启动模式,还有更多功能细节新增和优化
- 优化了PicGo的界面,解锁了窗口大小限制,同时美化了部分界面布局
- mac平台安装包已签名,从源头解决了PicGo上的安装包已损坏的日经问题
@@ -49,10 +50,7 @@ PicList的内核使用的是原版PicGo-Core基础上修改的[PicList-core](htt
**Typora 1.6.0-dev以及以上版本现在已经原生支持PicList了**
下载地址
[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")
[下载地址](https://typora.io/releases/all)
#### 1.6.0版本以下
@@ -74,7 +72,7 @@ MacOS:
### 如何在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)
@@ -92,10 +90,15 @@ MacOS:
| S3 API兼容平台 | ✔️ | ✔️ |
| 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
```
### 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://user-images.githubusercontent.com/96409857/222900656-6bb33045-6672-4c4d-ac34-1b9ba86011cc.png)
![image](https://user-images.githubusercontent.com/96409857/220510112-e524f270-ab56-4e8b-bfb2-eb0a77e559ef.png)
![image](https://user-images.githubusercontent.com/96409857/220510176-8a3f9f19-9182-4b56-b943-fc408ef63f22.png)
![image](https://user-images.githubusercontent.com/96409857/220510302-f193fc77-db1b-4817-81ff-3ab1c3a1f4d3.png)
![image](https://user-images.githubusercontent.com/96409857/220510371-a2fad42e-8063-4014-a691-ca5b66b8cc60.png)
![image](https://user-images.githubusercontent.com/96409857/220510427-b85ffc0a-55cf-43f1-b1b0-ba7776a75de2.png)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/1b76c0c4-753c-4d66-aa24-f805f9c2da15)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/56cf838a-a2eb-40af-96d4-1ffea25400af)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/bca7688a-e07f-4e80-9edd-c224298fa8ab)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/3e48e03d-b0b2-49e2-92a6-a52e0884677d)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/29de0046-1aef-4b28-95a6-b26c6e297c6f)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/e1c04488-2d3a-4e8f-aa26-ce41d0a383e2)
## 微信交流群
@@ -148,8 +136,8 @@ brew uninstall piclist
1. 你需要有 Node、Git 环境,了解 npm 的相关知识。
2. git clone [https://github.com/Kuingsmile/PicList.git](https://github.com/Kuingsmile/PicList.git) 并进入项目。
`yarn` 下载依赖
注意如果你没有yarn,请去 官网 下载安装后再使用。 用 npm install 将导致未知错误!
3. Mac 需要有 Xcode 环境,Windows 需要有 VS 环境。
注意如果你没有`yarn`,请去 官网 下载安装后再使用。 用 `npm install` 将导致未知错误!
3. Mac 需要有 `Xcode` 环境,Windows 需要有 `VS` 环境。
4. 如果需要贡献代码,可以参考[贡献指南](https://github.com/Kuingsmile/PicList/blob/dev/CONTRIBUTING.md)。
### 开发模式
@@ -158,6 +146,7 @@ brew uninstall piclist
`ctrl+c` # 退出开发模式
`yarn run dev` # 重新进入开发模式
注:Windows 开发模式运行之后会在底部任务栏的右下角应用区出现 PicList 的应用图标。
### 生产模式
+33 -31
View File
@@ -16,7 +16,8 @@
[简体中文](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
@@ -30,19 +31,19 @@ if you want to use PicList-core, please go to [https://github.com/Kuingsmile/Pic
## Features
- Maintain all the functions of PicGo, compatible with the existing PicGo plug-in system, including the combination with typora, obsidian and other software
- Add support for webdav upload, imgur account upload, local path upload, etc.
- Synchronous cloud deletion of pictures in the album, advanced search and sorting features have been added, along with the ability to bulk modify URLs.
- 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
- Add the configuration of multi-device synchronization function.
- Add
- 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
- Retains all the features of PicGo and is compatible with the vast majority of existing PicGo plugins, including integrations with software like Typora and Obsidian.
- Added multiple built-in image hosting platforms, such as WebDav, local image hosting, and SFTP. The original built-in imgur image host now also supports account login for uploading.
- Within the album, you can synchronize the deletion of cloud images. This is supported across all built-in image hosts and multiple plugins.
- The album now offers advanced search and sorting features, as well as batch URL modification.
- Built-in tools for adding watermarks, compressing images, scaling images, rotating images, and converting image formats are now available. Advanced renaming is also supported.
- Configuration can be synchronized to Github or Gitee repositories.
- Manages over ten types of image hosting platforms, allowing online viewing of cloud directories, file searching, batch uploading, batch downloading, file deletion, and more.
- Support previewing multiple formats of files, including pictures, videos, plain text files and markdown files, etc. For the specific formats supported, please refer to [Supported file format list](https://github.com/Kuingsmile/PicList/blob/dev/supported_format.md)
- Support batch cloud file renaming based on regular expressions
- The management interface uses the built-in database cache directory to accelerate the directory loading speed
- Support automatic update, no need to download manually every time, support multiple startup mode selection, and more function details are added and optimized
- Optimized the PicGo interface, unlocked the window size limit, and beautified the interface layout
- 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
- Supports the use of regular expressions for batch renaming of cloud files.
- For private storage buckets, pre-signed link copying for sharing is available.
- Software auto-updates are available, along with multiple startup modes, and many other feature details have been added and optimized.
- The PicGo interface has been enhanced, window size restrictions have been unlocked, and some interface layouts have been beautified.
- The installation package for the Mac platform is now signed, addressing the recurring issue on PicGo where the installation package was reported as damaged.
### How to use 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**
download link:
[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)
[download link](https://typora.io/releases/all)
#### **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
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)
@@ -94,10 +91,15 @@ Search and install `Image auto upload Plugin` in the community plugin, then ente
| S3 API compatible platform | ✔️ | ✔️ |
| WebDAV | ✔️ | ✔️ |
| Local | ✔️ | ✔️ |
| Built-in SFTP | ✔️ | ✔️ |
| Doge Cloud | ✔️ | ✔️ |
| Plugin | Album cloud deletion |
| :----------------------------------------------------------: | :------------------: |
| [picgo-plugin-s3](https://github.com/wayjam/picgo-plugin-s3) | ✔️ |
| Plugin | Album cloud deletion |
| :----------------------------------------------------------------------------------------: | :------------------: |
| [picgo-plugin-s3](https://github.com/wayjam/picgo-plugin-s3) | ✔️ |
| [picgo-plugin-alist](https://github.com/jinzhi0123/picgo-plugin-alist) | ✔️ |
| [picgo-plugin-huawei-uploader](https://github.com/YunfengGao/picgo-plugin-huawei-uploader) | ✔️ |
| [picgo-plugin-dogecloud](https://github.com/w4j1e/picgo-plugin-dogecloud) | ✔️ |
## Download and install
@@ -135,13 +137,12 @@ Thank you again for your support for PicList.
## Application screenshot
![image](https://user-images.githubusercontent.com/96409857/222900642-f1d04a41-f025-4f3c-b838-bae770e0b929.png)
![image](https://user-images.githubusercontent.com/96409857/222900656-6bb33045-6672-4c4d-ac34-1b9ba86011cc.png)
![image](https://user-images.githubusercontent.com/96409857/220510112-e524f270-ab56-4e8b-bfb2-eb0a77e559ef.png)
![image](https://user-images.githubusercontent.com/96409857/220510176-8a3f9f19-9182-4b56-b943-fc408ef63f22.png)
![image](https://user-images.githubusercontent.com/96409857/220510302-f193fc77-db1b-4817-81ff-3ab1c3a1f4d3.png)
![image](https://user-images.githubusercontent.com/96409857/220510371-a2fad42e-8063-4014-a691-ca5b66b8cc60.png)
![image](https://user-images.githubusercontent.com/96409857/220510427-b85ffc0a-55cf-43f1-b1b0-ba7776a75de2.png)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/1b76c0c4-753c-4d66-aa24-f805f9c2da15)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/56cf838a-a2eb-40af-96d4-1ffea25400af)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/bca7688a-e07f-4e80-9edd-c224298fa8ab)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/3e48e03d-b0b2-49e2-92a6-a52e0884677d)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/29de0046-1aef-4b28-95a6-b26c6e297c6f)
![image](https://github.com/Kuingsmile/PicList/assets/96409857/e1c04488-2d3a-4e8f-aa26-ce41d0a383e2)
## 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.
2. git clone [https://github.com/Kuingsmile/PicList.git](https://github.com/Kuingsmile/PicList.git) and enter the project.
`yarn` download dependencies
Note that if you don't have yarn, please go to the official website to download and install it before using it. Using npm install will cause unknown errors!
3. Mac needs Xcode environment, Windows needs VS environment.
Note that if you don't have `yarn`, please go to the official website to download and install it before using it. Using `npm install` will cause unknown errors!
3. Mac needs `Xcode` environment, Windows needs `VS` environment.
4. If you need to contribute code, you can refer to [contribution guide](https://github.com/Kuingsmile/PicList/blob/dev/CONTRIBUTING_EN.md)
### Development mode
@@ -162,6 +163,7 @@ Enter `yarn run dev` to enter development mode, which has hot reload feature. Ho
`ctrl+c` # Exit development mode
`yarn run dev` # Re-enter development mode
Note: After the development mode is running, the application icon of PicList will appear in the application area of the taskbar in the lower right corner of the bottom bar.
### Production mode
+14 -10
View File
@@ -5,7 +5,7 @@
"name": "Kuingsmile",
"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",
"bugs": {
"url": "https://github.com/Kuingsmile/PicList/issues",
@@ -32,26 +32,27 @@
"sha256": "node ./scripts/gen-sha256.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.272.0",
"@aws-sdk/lib-storage": "^3.272.0",
"@aws-sdk/s3-request-presigner": "^3.272.0",
"@aws-sdk/client-s3": "^3.388.0",
"@aws-sdk/lib-storage": "^3.388.0",
"@aws-sdk/s3-request-presigner": "^3.388.0",
"@element-plus/icons-vue": "^2.1.0",
"@highlightjs/vue-plugin": "^2.1.0",
"@nodelib/fs.walk": "^2.0.0",
"@octokit/rest": "^19.0.7",
"@picgo/i18n": "^1.0.0",
"@picgo/store": "^2.0.4",
"@smithy/node-http-handler": "^2.0.2",
"@types/marked": "^4.0.8",
"@types/mime-types": "^2.1.1",
"@videojs-player/vue": "^1.0.0",
"ali-oss": "^6.17.1",
"aws-sdk": "^2.1373.0",
"ali-oss": "^6.18.0",
"axios": "^1.4.0",
"compare-versions": "^4.1.3",
"core-js": "^3.27.1",
"cos-nodejs-sdk-v5": "^2.12.1",
"cos-nodejs-sdk-v5": "^2.12.4",
"dexie": "^3.2.4",
"electron-updater": "^6.1.1",
"element-plus": "^2.3.8",
"element-plus": "^2.3.9",
"epipebomb": "^1.0.0",
"fast-xml-parser": "^4.2.5",
"form-data": "^4.0.0",
@@ -65,12 +66,14 @@
"marked": "^4.3.0",
"mime-types": "^2.1.35",
"mitt": "^3.0.0",
"node-ssh-no-cpu-features": "^1.0.1",
"nodejs-file-downloader": "^4.12.1",
"piclist": "^0.8.5",
"piclist": "^0.8.12",
"pinia": "^2.1.4",
"pinia-plugin-persistedstate": "^3.1.0",
"qiniu": "^7.8.0",
"qiniu": "^7.9.0",
"qrcode.vue": "^3.4.0",
"querystring": "^0.2.1",
"shell-path": "2.1.0",
"upyun": "^3.4.6",
"uuid": "^9.0.0",
@@ -120,6 +123,7 @@
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^9.9.0",
"husky": "^3.1.0",
"node-loader": "^2.0.0",
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2",
"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_MANAGE_CONFIG: Manage configuration
SETTINGS_AUTO_IMPORT: Auto import config in manage page
SETTINGS_AUTO_IMPORT_SELECT_PICBED: Select picbed
SETTINGS_TAB_SYSTEM: System
SETTINGS_TAB_SYNC_CONFIG: Sync and Configuration
SETTINGS_TAB_UPLOAD: Upload
@@ -322,6 +323,7 @@ PLUGIN_INSTALLED: Installed
PLUGIN_DOING_SOMETHING: Doing...
PLUGIN_LIST: Plugin List
PLUGIN_IMPORT_LOCAL: Import Local Plugins
PLUGIN_UPDATE_ALL: Update All Plugins
# 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_BASE_DIR_DESC: Base directory - Optional
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_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.
@@ -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_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_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_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
@@ -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_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_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...
@@ -782,6 +844,7 @@ MANAGE_BUCKET_DELETE_SUCCESS: Deletion successful
MANAGE_BUCKET_DELETE_FAIL: Deletion failed
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_SUCCESS: Rename successful
MANAGE_BUCKET_RENAME_ERROR_MSG: Rename failed
MANAGE_BUCKET_DOWNLOAD_COLUMN_FILENAME: File name
MANAGE_BUCKET_DOWNLOAD_COLUMN_FINISHTIME: Completion time
+64
View File
@@ -278,6 +278,7 @@ SETTINGS_SYNC_DOWNLOAD_FAILED: 下载失败
SETTINGS_SYNC_COMMON_CONFIG: 通用配置
SETTINGS_SYNC_MANAGE_CONFIG: 管理配置
SETTINGS_AUTO_IMPORT: 管理页面自动导入配置
SETTINGS_AUTO_IMPORT_SELECT_PICBED: 选择需要开启自动导入的图床
SETTINGS_TAB_SYSTEM: 系统设置
SETTINGS_TAB_SYNC_CONFIG: 同步与配置
SETTINGS_TAB_UPLOAD: 上传设置
@@ -324,6 +325,7 @@ PLUGIN_INSTALLED: 已安装
PLUGIN_DOING_SOMETHING: 进行中
PLUGIN_LIST: 插件列表
PLUGIN_IMPORT_LOCAL: 导入本地插件
PLUGIN_UPDATE_ALL: 更新全部插件
# tips
@@ -577,6 +579,8 @@ MANAGE_CONSTANT_S3_BUCKET_DESC: 存储桶名-可选
MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: 英文逗号分隔,例如:bucket1,bucket2
MANAGE_CONSTANT_S3_BASE_DIR_DESC: 起始目录-可选
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_ITEMS_PAGE_DESC: 每页显示数量
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_TOOLTIP: 如果您的WebDAV服务器支持自定义域名,请填写
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_PLACEHOLDER: '例如:http://127.0.0.1:1080'
MANAGE_CONSTANT_WEBDAV_PROXY_TOOLTIP: 如果需要特殊网络环境才能访问,请使用代理
@@ -609,6 +616,62 @@ MANAGE_CONSTANT_WEBDAV_SSL_TOOLTIP: 根据WebDAV服务器的配置,如果您
MANAGE_CONSTANT_WEBDAV_EXPLAIN: 'WebDAV配置'
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_DESC: 点击图标和别名可查看详情,点击进入可查看文件页面,点击删除可删除配置
MANAGE_LOGIN_PAGE_PANE_LOADING: 导入配置...
@@ -785,6 +848,7 @@ MANAGE_BUCKET_DELETE_SUCCESS: 删除成功
MANAGE_BUCKET_DELETE_FAIL: 删除失败
MANAGE_BUCKET_DELETE_CANCEL: 已取消删除
MANAGE_BUCKET_RENAME_INFO_MSG: 新文件名与原文件名相同,无需重命名
MANAGE_BUCKET_RENAME_SUCCESS: 重命名成功
MANAGE_BUCKET_RENAME_ERROR_MSG: 重命名失败
MANAGE_BUCKET_DOWNLOAD_COLUMN_FILENAME: 文件名
MANAGE_BUCKET_DOWNLOAD_COLUMN_FINISHTIME: 完成时间
+63
View File
@@ -276,6 +276,7 @@ SETTINGS_SYNC_DOWNLOAD_FAILED: 下載失敗
SETTINGS_SYNC_COMMON_CONFIG: 通用配置
SETTINGS_SYNC_MANAGE_CONFIG: 管理配置
SETTINGS_AUTO_IMPORT: 管理頁面自動導入配置
SETTINGS_AUTO_IMPORT_SELECT_PICBED: 選擇需要開啟自動導入的圖床
SETTINGS_TAB_SYSTEM: 系統設置
SETTINGS_TAB_SYNC_CONFIG: 同步與配置
SETTINGS_TAB_UPLOAD: 上傳設置
@@ -322,6 +323,7 @@ PLUGIN_INSTALLED: 已安裝
PLUGIN_DOING_SOMETHING: 進行中
PLUGIN_LIST: 插件列表
PLUGIN_IMPORT_LOCAL: 導入本地插件
PLUGIN_UPDATE_ALL: 更新全部插件
# tips
@@ -574,6 +576,8 @@ MANAGE_CONSTANT_S3_BUCKET_DESC: 存儲桶名-可選
MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: 英文逗號分隔,例如:bucket1,bucket2
MANAGE_CONSTANT_S3_BASE_DIR_DESC: 起始目錄-可選
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_ITEMS_PAGE_DESC: 每頁顯示數量
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_TOOLTIP: 如果您的WebDAV伺服器支援自訂網域名稱,請填寫
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_PLACEHOLDER: '例如:http://127.0.0.1:1080'
MANAGE_CONSTANT_WEBDAV_PROXY_TOOLTIP: 如果需要特殊網路環境才能訪問,請使用代理
@@ -606,6 +613,61 @@ MANAGE_CONSTANT_WEBDAV_SSL_TOOLTIP: 根據WebDAV伺服器的配置,如果您
MANAGE_CONSTANT_WEBDAV_EXPLAIN: 'WebDAV配置'
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_DESC: 點擊圖標和別名可查看詳情,點擊進入可查看檔案頁面,點擊刪除可刪除配置
MANAGE_LOGIN_PAGE_PANE_LOADING: 導入配置...
@@ -782,6 +844,7 @@ MANAGE_BUCKET_DELETE_SUCCESS: 刪除成功
MANAGE_BUCKET_DELETE_FAIL: 刪除失敗
MANAGE_BUCKET_DELETE_CANCEL: 已取消删除
MANAGE_BUCKET_RENAME_INFO_MSG: 新文件名和原文件名相同,無需重命名
MANAGE_BUCKET_RENAME_SUCCESS: 重命名成功
MANAGE_BUCKET_RENAME_ERROR_MSG: 重命名失敗
MANAGE_BUCKET_DOWNLOAD_COLUMN_FILENAME: 文件名
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
},
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
sslEnabled: true,
region: 'us-east-1'
tls: true,
region: 'auto'
}
const client = new S3Client.S3Client(options)
const parallelUploads3 = new Upload.Upload({
+80 -63
View File
@@ -1,6 +1,6 @@
// upload dist bundled-app to r2
require('dotenv').config()
const S3 = require('aws-sdk/clients/s3')
const S3Client = require('@aws-sdk/client-s3')
const Upload = require('@aws-sdk/lib-storage')
const pkg = require('../package.json')
@@ -8,6 +8,7 @@ const configList = require('./config')
const fs = require('fs')
const path = require('path')
const yaml = require('js-yaml')
const mime = require('mime-types')
const BUCKET = 'piclist-dl'
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_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`,
accessKeyId: SECRET_ID,
secretAccessKey: SECRET_KEY,
signatureVersion: 'v4'
})
tls: true,
region: 'auto'
}
const removeDupField = path => {
const file = fs.readFileSync(path, 'utf8')
@@ -39,74 +43,87 @@ const removeDupField = path => {
const uploadFile = async () => {
try {
const platform = process.platform
if (configList[platform]) {
let versionFileHasUploaded = false
for (const [index, config] of configList[platform].entries()) {
const fileName = `${config.appNameWithPrefix}${VERSION}${config.arch}${config.ext}`
const distPath = path.join(__dirname, '../dist_electron')
const versionFileName = config['version-file']
console.log('[PicList Dist] Uploading...', fileName, `${index + 1}/${configList[platform].length}`)
const fileStream = fs.createReadStream(path.join(distPath, fileName))
const options = {
credentials: {
accessKeyId: SECRET_ID,
secretAccessKey: SECRET_KEY
},
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
sslEnabled: true,
region: 'us-east-1'
if (!configList[platform]) {
console.warn('platform not supported!', platform)
return
}
let versionFileHasUploaded = false
for (const [index, config] of configList[platform].entries()) {
const fileName = `${config.appNameWithPrefix}${VERSION}${config.arch}${config.ext}`
const distPath = path.join(__dirname, '../dist_electron')
const versionFileName = config['version-file']
console.log('[PicList Dist] Uploading...', fileName, `${index + 1}/${configList[platform].length}`)
const fileStream = fs.createReadStream(path.join(distPath, fileName))
const client = new S3Client.S3Client(options)
const parallelUploads3 = new Upload.Upload({
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,
params: {
Bucket: BUCKET,
Key: `${FILE_PATH}${fileName}`,
Body: fileStream,
ContentType: 'application/octet-stream',
Key: `${versionFileName}`,
Body: versionFileStream,
ContentType: mime.lookup(versionFileName),
Metadata: {
description: 'uploaded by PicList'
}
}
})
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')
console.log('\nUploading version file to root...')
await uploadVersionFileToRoot.done()
console.log(`${versionFileName} uploaded!`)
versionFileStream.close()
const versionFileStream2 = fs.createReadStream(versionFilePath)
const uploadVersionFileToLatest = new Upload.Upload({
client,
params: {
Bucket: BUCKET,
Key: `${FILE_PATH}${versionFileName}`,
Body: versionFileStream2,
ContentType: mime.lookup(versionFileName),
Metadata: {
description: 'uploaded by PicList'
}
}
removeDupField(versionFilePath)
const versionFileBuffer = fs.readFileSync(versionFilePath)
await s3
.upload({
Bucket: BUCKET,
Key: `${versionFileName}`,
Body: versionFileBuffer
})
.promise()
await s3
.upload({
Bucket: BUCKET,
Key: `${FILE_PATH}${versionFileName}`,
Body: versionFileBuffer
})
.promise()
versionFileHasUploaded = true
}
})
console.log('\nUploading version file to latest...')
await uploadVersionFileToLatest.done()
console.log(`${versionFileName} uploaded!`)
versionFileStream2.close()
versionFileHasUploaded = true
}
} else {
console.warn('platform not supported!', platform)
}
} catch (err) {
console.error(err)
+30 -10
View File
@@ -1,25 +1,43 @@
// Vue 相关
import { createApp } from 'vue'
import App from './renderer/App.vue'
import router from './renderer/router'
import ElementUI from 'element-plus'
import 'element-plus/dist/index.css'
import { webFrame } from 'electron'
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 '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 piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// 代码高亮
import 'highlight.js/styles/atom-one-dark.css'
import hljsVuePlugin from '@highlightjs/vue-plugin'
import hljsCommon from 'highlight.js/lib/common'
import VueVideoPlayer from '@videojs-player/vue'
import 'video.js/dist/video-js.css'
webFrame.setVisualZoomLevelLimits(1, 1)
@@ -34,7 +52,9 @@ app.config.globalProperties.$builtInPicBed = [
'upyun',
'aliyun',
'github',
'webdavplist'
'webdavplist',
'local',
'sftpplist'
]
app.config.unwrapInjectedRef = true
+16 -7
View File
@@ -1,15 +1,25 @@
// get notice from remote
// such as some notices for users; some updates for users
// External dependencies
import axios from 'axios'
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 axios from 'axios'
// Electron modules
import { app, clipboard, dialog, shell } from 'electron'
// Custom modules and utilities
import windowManager from '../window/windowManager'
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
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')
this.remoteNoticeLocalCountStorage = localCountStorage
} catch (e) {
console.log(e)
this.remoteNoticeLocalCountStorage = localCountStorage
}
}
+15 -5
View File
@@ -1,14 +1,24 @@
import bus from '@core/bus'
// External dependencies
import {
globalShortcut
} 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 { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import shortKeyService from './shortKeyService'
import logger from '@core/picgo/logger'
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 {
private isInModifiedMode: boolean = false
constructor () {
@@ -1,4 +1,5 @@
import logger from '@core/picgo/logger'
class ShortKeyService {
private commandList: Map<string, IShortKeyHandler> = new Map()
registerCommand (command: string, handler: IShortKeyHandler) {
+7 -1
View File
@@ -1,4 +1,8 @@
// External dependencies
import fs from 'fs-extra'
import { cloneDeep } from 'lodash'
// Electron modules
import {
app,
Menu,
@@ -9,6 +13,8 @@ import {
screen,
nativeTheme
} from 'electron'
// Custom utilities and modules
import uploader from 'apis/app/uploader'
import db, { GalleryDB } from '~/main/apis/core/datastore'
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 picgo from '../../core/picgo'
import { uploadClipboardFiles } from '../uploader/apis'
import { cloneDeep } from 'lodash'
let contextMenu: Menu | 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 {
Notification,
WebContents
} from 'electron'
// Custom utilities and modules
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import uploader from '.'
import pasteTemplate from '~/main/utils/pasteTemplate'
import db, { GalleryDB } from '~/main/apis/core/datastore'
import { handleCopyUrl, handleUrlEncodeWithSetting } from '~/main/utils/common'
@@ -12,8 +17,13 @@ import { T } from '~/main/i18n/index'
import ALLApi from '@/apis/allApi'
import picgo from '@core/picgo'
import GuiApi from '../../gui'
import fs from 'fs-extra'
import { cloneDeep } from 'lodash'
import uploader from '.'
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 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[]> => {
const result = []
const picBedsCanbeDeleted = ['smms', 'github', 'imgur', 'tcyun', 'aliyun', 'qiniu', 'upyun', 'aws-s3', 'webdavplist']
for (const item of list) {
if (item.id) {
try {
@@ -130,23 +154,23 @@ export const deleteChoosedFiles = async (list: ImgInfo[]): Promise<boolean[]> =>
const file = await dbStore.removeById(item.id)
if (await picgo.getConfig('settings.deleteCloudFile')) {
if (item.type !== undefined && picBedsCanbeDeleted.includes(item.type)) {
setTimeout(() => {
ALLApi.delete(item).then((value: boolean) => {
if (value) {
const notification = new Notification({
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()
}
const noteFunc = (value: boolean) => {
const notification = new Notification({
title: T('MANAGE_BUCKET_BATCH_DELETE_ERROR_MSG_MSG2'),
body: T(value ? 'GALLERY_SYNC_DELETE_NOTICE_SUCCEED' : 'GALLERY_SYNC_DELETE_NOTICE_FAILED')
})
}, 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(() => {
+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 {
Notification,
BrowserWindow,
@@ -5,22 +13,26 @@ import {
WebContents,
clipboard
} from 'electron'
import dayjs from 'dayjs'
// Custom utilities and modules
import picgo from '@core/picgo'
import db from '~/main/apis/core/datastore'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import util from 'util'
import { IPicGo } from 'piclist'
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 { 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'
// 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> => {
return new Promise((resolve) => {
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 {
SETTING_WINDOW_URL,
TRAY_WINDOW_URL,
@@ -5,15 +16,13 @@ import {
RENAME_WINDOW_URL,
TOOLBOX_WINDOW_URL
} from './constants'
// Custom types/enums
import { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum'
import bus from '@core/bus'
// External utility functions
import { CREATE_APP_MENU } from '@core/bus/constants'
import db from '~/main/apis/core/datastore'
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>()
+8 -3
View File
@@ -1,7 +1,12 @@
import {
BrowserWindow
} from 'electron'
// External dependencies
import windowList from './windowList'
// Electron modules
import { BrowserWindow } from 'electron'
// Custom utilities and modules
// Custom types/enums
import { IWindowList } from '#/types/enum'
class WindowManager implements IWindowManager {
+18 -4
View File
@@ -1,10 +1,24 @@
// External dependencies
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 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'
const STORE_PATH = app.getPath('userData')
const configFilePath = path.join(STORE_PATH, 'data.json')
const configFileBackupPath = path.join(STORE_PATH, 'data.bak.json')
+11
View File
@@ -1,6 +1,17 @@
// External dependencies
import fs from 'fs-extra'
// Electron modules
// Custom utilities and modules
import { dbPathChecker, dbPathDir, getGalleryDBPath } from './dbChecker'
// Custom types/enums
// External utility functions
import { DBStore, JSONStore } from '@picgo/store'
// External utility functions
import { T } from '~/main/i18n'
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 debounce from 'lodash/debounce'
// Electron modules
// Custom utilities and modules
import { PicGo } from 'piclist'
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()
+13 -9
View File
@@ -1,25 +1,29 @@
// External dependencies
import fs from 'fs-extra'
import { cloneDeep } from 'lodash'
// Electron modules
import {
dialog,
BrowserWindow,
Notification,
ipcMain
} from 'electron'
// Custom utilities and modules
import db, { GalleryDB } from 'apis/core/datastore'
import { dbPathChecker, defaultConfigPath, getGalleryDBPath } from 'apis/core/datastore/dbChecker'
import uploader from 'apis/app/uploader'
import pasteTemplate from '~/main/utils/pasteTemplate'
import { handleCopyUrl } from '~/main/utils/common'
import {
getWindowId,
getSettingWindowId
} from '@core/bus/apis'
import {
SHOW_INPUT_BOX
} from '~/universal/events/constants'
import { getWindowId, getSettingWindowId } from '@core/bus/apis'
import { SHOW_INPUT_BOX } from '~/universal/events/constants'
// Custom types/enums
// External utility functions
import { DBStore } from '@picgo/store'
import { T } from '~/main/i18n'
import fs from 'fs-extra'
import { cloneDeep } from 'lodash'
// Cross-process support may be required in the future
class GuiApi implements IGuiApi {
+11 -1
View File
@@ -1,4 +1,9 @@
// External dependencies
import bus from '@core/bus'
// Electron modules
// Custom utilities and modules
import {
uploadClipboardFiles,
uploadChoosedFiles
@@ -6,8 +11,12 @@ import {
import {
createMenu
} from 'apis/app/system'
import { IWindowList } from '#/types/enum'
import windowManager from 'apis/app/window/windowManager'
// Custom types/enums
import { IWindowList } from '#/types/enum'
// External utility functions
import {
UPLOAD_WITH_FILES,
UPLOAD_WITH_FILES_RESPONSE,
@@ -19,6 +28,7 @@ import {
GET_SETTING_WINDOW_ID_RESPONSE,
CREATE_APP_MENU
} from '@core/bus/constants'
function initEventCenter () {
const eventList: any = {
'picgo:upload': uploadClipboardFiles,
+84
View File
@@ -1,3 +1,4 @@
// Electron 相关
import {
app,
ipcMain,
@@ -8,16 +9,38 @@ import {
screen,
IpcMainInvokeEvent
} from 'electron'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum'
// 上传器
import uploader from 'apis/app/uploader'
// 粘贴模板函数
import pasteTemplate from '~/main/utils/pasteTemplate'
// 数据存储库和类型声明
import db, { GalleryDB } from '~/main/apis/core/datastore'
// 服务器模块
import server from '~/main/server'
// 获取图片床模块
import getPicBeds from '~/main/utils/getPicBeds'
// 快捷键处理器
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
// 全局事件总线
import bus from '@core/bus'
// 文件系统库
import fs from 'fs-extra'
// 事件常量
import {
TOGGLE_SHORTKEY_MODIFIED_MODE,
OPEN_DEVTOOLS,
@@ -34,22 +57,45 @@ import {
GET_PICBEDS,
HIDE_DOCK
} from '#/events/constants'
// 上传剪贴板文件和已选文件的函数
import {
uploadClipboardFiles,
uploadChoosedFiles
} from '~/main/apis/app/uploader/apis'
// 核心 IPC 模块
import picgoCoreIPC from './picgoCoreIPC'
// 处理复制的 URL 和生成短链接的函数
import { handleCopyUrl, generateShortUrl } from '~/main/utils/common'
// 构建主页面、迷你页面、插件页面、图片床列表的菜单函数
import { buildMainPageMenu, buildMiniPageMenu, buildPluginPageMenu, buildPicBedListMenu } from './remotes/menu'
// 路径处理库
import path from 'path'
// i18n 模块
import { T } from '~/main/i18n'
// 同步设置的上传和下载文件函数
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')
export default {
listen () {
picgoCoreIPC.listen()
// Upload Related IPC
// from macOS tray
ipcMain.on('uploadClipboardFiles', async () => {
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
@@ -85,6 +131,7 @@ export default {
return uploadChoosedFiles(evt.sender, files)
})
// ShortKey Related IPC
ipcMain.on('updateShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, oldKey: string, from: string) => {
const result = shortKeyHandler.updateShortKey(item, oldKey, from)
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 () => {
const picGoConfigPath = STORE_PATH.replace('piclist', 'picgo')
const fileToMigration = [
@@ -147,6 +227,8 @@ export default {
}
})
// PicList Setting page IPC
ipcMain.on('updateCustomLink', () => {
const notification = new Notification({
title: T('OPERATION_SUCCEED'),
@@ -212,6 +294,8 @@ export default {
mainWindow.setAlwaysOnTop(!isAlwaysOnTop)
})
// Window operation API
ipcMain.on('openSettingWindow', () => {
windowManager.get(IWindowList.SETTING_WINDOW)!.show()
const autoCloseMiniWindow = db.get('settings.autoCloseMiniWindow') || false
+25 -10
View File
@@ -1,5 +1,5 @@
// External dependencies
import path from 'path'
import GuiApi from 'apis/gui'
import {
dialog,
shell,
@@ -7,7 +7,11 @@ import {
ipcMain,
clipboard
} 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 picgo from '@core/picgo'
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 { showNotification } from '~/main/utils/common'
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 {
PICGO_SAVE_CONFIG,
PICGO_GET_CONFIG,
@@ -32,12 +46,6 @@ import {
GET_CURRENT_LANGUAGE
} 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
const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require
// const PluginHandler = requireFunc('picgo/lib/PluginHandler').default
@@ -188,10 +196,10 @@ const handlePluginUninstall = async (fullName: string) => {
dispose()
}
const handlePluginUpdate = async (fullName: string) => {
const handlePluginUpdate = async (fullName: string | string[]) => {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const dispose = handleNPMError()
const res = await picgo.pluginHandler.update([fullName])
const res = await picgo.pluginHandler.update(typeof fullName === 'string' ? [fullName] : fullName)
if (res.success) {
window.webContents.send('updateSuccess', res.body[0])
} else {
@@ -204,6 +212,12 @@ const handlePluginUpdate = async (fullName: string) => {
dispose()
}
const handleUpdateAllPlugin = () => {
ipcMain.on('updateAllPlugin', async (event: IpcMainEvent, list: string[]) => {
handlePluginUpdate(list)
})
}
const handleNPMError = (): IDispose => {
const handler = (msg: string) => {
if (msg === 'NPM is not installed') {
@@ -411,6 +425,7 @@ export default {
handlePicGoGetConfig()
handlePicGoGalleryDB()
handleImportLocalPlugin()
handleUpdateAllPlugin()
handleOpenFile()
handleOpenWindow()
handleI18n()
+26 -9
View File
@@ -1,20 +1,37 @@
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
// External dependencies
import pkg from 'root/package.json'
// Electron modules
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 picgo from '@core/picgo'
import {
uploadClipboardFiles
} from '~/main/apis/app/uploader/apis'
import pkg from 'root/package.json'
import GuiApi from 'apis/gui'
import { PICGO_CONFIG_PLUGIN, PICGO_HANDLE_PLUGIN_DONE, PICGO_HANDLE_PLUGIN_ING, PICGO_TOGGLE_PLUGIN, SHOW_MAIN_PAGE_DONATION, SHOW_MAIN_PAGE_QRCODE } from '~/universal/events/constants'
import picgoCoreIPC from '~/main/events/picgoCoreIPC'
import { PicGo as PicGoCore } from 'piclist'
import { T } from '~/main/i18n'
import { changeCurrentUploader } from '~/main/utils/handleUploaderConfig'
import db from '~/main/apis/core/datastore'
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 {
label: string
handle: (arg0: PicGoCore, arg1: GuiApi) => Promise<void>
+11 -2
View File
@@ -1,10 +1,19 @@
// External dependencies
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 { toolboxRouter } from './routes/toolbox'
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 {
private routes: IRPCRoutes = new Map()
+7 -1
View File
@@ -1,6 +1,12 @@
import { IRPCActionType } from '~/universal/types/enum'
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()
+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 { RPCRouter } from '../router'
import { app, clipboard, shell } from 'electron'
const systemRouter = new RPCRouter()
@@ -1,9 +1,18 @@
// External dependencies
import fs from 'fs-extra'
import path from 'path'
// Electron modules
// Custom utilities and modules
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'
// 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'
const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD)
@@ -1,10 +1,19 @@
// External dependencies
import fs from 'fs-extra'
import path from 'path'
// Electron modules
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 { 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'
export const checkFileMap: IToolboxCheckerMap<
@@ -1,10 +1,21 @@
// External dependencies
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 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'
const getProxy = (proxyStr: string): AxiosRequestConfig['proxy'] | false => {
+5
View File
@@ -1,6 +1,11 @@
// External dependencies
import http from 'http'
import fs from 'fs-extra'
import path from 'path'
// Electron modules
// Custom utilities and modules
import picgo from '@core/picgo'
import logger from '../apis/core/picgo/logger'
+9
View File
@@ -1,7 +1,16 @@
// External dependencies
import yaml from 'js-yaml'
import { ObjectAdapter, I18n } from '@picgo/i18n'
import path from 'path'
import fs from 'fs-extra'
// Electron modules
// Custom utilities and modules
// Custom types/enums
// External utility functions
import { builtinI18nList } from '#/i18n'
class I18nManager {
+16 -9
View File
@@ -15,10 +15,6 @@ import ipcList from '~/main/events/ipcList'
import busEventList from '~/main/events/busEventList'
import { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum'
import windowManager from 'apis/app/window/windowManager'
import {
updateShortKeyFromVersion212,
migrateGalleryFromVersion230
} from '~/main/migrate'
import {
uploadChoosedFiles,
uploadClipboardFiles
@@ -29,7 +25,7 @@ import {
import server from '~/main/server/index'
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
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 logger from 'apis/core/picgo/logger'
import picgo from 'apis/core/picgo'
@@ -62,9 +58,8 @@ const handleStartUpFiles = (argv: string[], cwd: string) => {
uploadChoosedFiles(win.webContents, files)
}
return true
} else {
return false
}
return false
}
autoUpdater.setFeedURL({
@@ -90,9 +85,19 @@ autoUpdater.on('update-available', (info: UpdateInfo) => {
autoUpdater.downloadUpdate()
}
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', () => {
dialog.showMessageBox({
type: 'info',
@@ -100,9 +105,13 @@ autoUpdater.on('update-downloaded', () => {
buttons: ['Yes', 'No'],
message: T('TIPS_UPDATE_DOWNLOADED')
}).then((result) => {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
window.webContents.send('updateProgress', { progress: 100 })
if (result.response === 0) {
autoUpdater.quitAndInstall()
}
}).catch((err) => {
logger.error(err)
})
})
@@ -122,8 +131,6 @@ class LifeCycle {
UpDownTaskQueue.getInstance()
manageIpcList.listen()
busEventList.listen()
updateShortKeyFromVersion212(db, db.get('settings.shortKey'))
await migrateGalleryFromVersion230(db, GalleryDB.getInstance(), picgo)
}
private onReady () {
+117 -143
View File
@@ -1,18 +1,37 @@
// Axios
import axios from 'axios'
// 加密函数、获取文件 MIME 类型、错误格式化函数、新的下载器、并发异步任务池
import { hmacSha1Base64, getFileMimeType, formatError, NewDownloader, ConcurrencyPromisePool } from '../utils/common'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron'
// 快速 XML 解析器
import { XMLParser } from 'fast-xml-parser'
// 阿里云 OSS 客户端库
import OSS from 'ali-oss'
// 路径处理库
import path from 'path'
// 是否为图片的判断函数
import { isImage } from '~/renderer/manage/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum'
import UpDownTaskQueue,
{
uploadTaskSpecialStatus,
commonTaskStatus
} from '../datastore/upDownTaskQueue'
// 上传下载任务队列
import UpDownTaskQueue, { uploadTaskSpecialStatus, commonTaskStatus } from '../datastore/upDownTaskQueue'
// 日志记录器
import { ManageLogger } from '../utils/logger'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
// 坑爹阿里云 返回数据类型标注和实际各种不一致
@@ -49,22 +68,20 @@ class AliyunApi {
}
formatFile (item: OSS.ObjectMeta, slicedPrefix: string, urlPrefix: string): any {
const result = {
const fileName = item.name.replace(slicedPrefix, '')
return {
...item,
key: item.name,
rawUrl: `${urlPrefix}/${item.name}`,
fileName: item.name.replace(slicedPrefix, ''),
fileName,
fileSize: item.size,
formatedTime: new Date(item.lastModified).toLocaleString(),
isDir: false,
checked: 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) {
@@ -100,47 +117,30 @@ class AliyunApi {
* 获取存储桶列表
*/
async getBucketList (): Promise<any> {
const formatItem = (item: OSS.Bucket) => {
return {
const getBuckets = async (marker?: string) => {
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,
Location: item.region,
CreationDate: item.creationDate
}
}
const res = await this.ctx.listBuckets({
'max-keys': 1000
}) as IStringKeyMap
const result = [] as IStringKeyMap[]
let NextMarker = ''
if (res.res.statusCode === 200) {
if (res.buckets) {
result.push(...res.buckets.map((item: OSS.Bucket) => formatItem(item)))
let isTruncated = res.isTruncated
NextMarker = res.nextMarker
while (isTruncated) {
const res = await this.ctx.listBuckets({
marker: NextMarker,
'max-keys': 1000
}) as IStringKeyMap
if (res.res.statusCode === 200) {
if (res.buckets) {
result.push(...res.buckets.map((item: OSS.Bucket) => formatItem(item)))
isTruncated = res.isTruncated
NextMarker = res.nextMarker
} else {
isTruncated = false
}
} else {
isTruncated = false
}
}
return result
} else {
return []
}
} else {
return []
}))
return { result: formattedBuckets, isTruncated: res.isTruncated, nextMarker: res.nextMarker }
}
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()
}
const authorization = this.authorization('GET', `/${param.bucketName}/?cname`, headers, '', '')
const res = await axios({
url: `https://${param.bucketName}.${param.region}.aliyuncs.com/?cname`,
method: 'GET',
@@ -159,25 +160,22 @@ class AliyunApi {
Authorization: authorization
}
})
if (res.status === 200) {
if (res?.status === 200) {
const parser = new XMLParser()
const result = parser.parse(res.data)
if (result.ListCnameResult && result.ListCnameResult.Cname) {
if (Array.isArray(result.ListCnameResult.Cname)) {
const cnameList = [] as string[]
result.ListCnameResult.Cname.forEach((item: IStringKeyMap) => {
item.Status === 'Enabled' && cnameList.push(item.Domain)
})
return cnameList
} else {
return result.ListCnameResult.Cname.Status === 'Enabled' ? [result.ListCnameResult.Cname.Domain] : []
}
} else {
return []
if (result.ListCnameResult?.Cname) {
const cnames = Array.isArray(result.ListCnameResult.Cname)
? result.ListCnameResult.Cname
: [result.ListCnameResult.Cname]
return cnames
.filter((item: IStringKeyMap) => item.Status === 'Enabled')
.map((item: IStringKeyMap) => item.Domain)
}
} else {
return []
}
return []
}
/**
@@ -209,7 +207,7 @@ class AliyunApi {
dataRedundancyType: 'LRS',
timeout: this.timeOut
})
return res && res.res.status === 200
return res?.res?.status === 200
}
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
@@ -240,8 +238,8 @@ class AliyunApi {
}, {
timeout: this.timeOut
})
if (res && res.res.statusCode === 200) {
res.objects && res.objects.forEach((item: OSS.ObjectMeta) => {
if (res?.res?.statusCode === 200) {
res?.objects?.forEach((item: OSS.ObjectMeta) => {
item.size !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
})
window.webContents.send(refreshDownloadFileTransferList, result)
@@ -288,11 +286,11 @@ class AliyunApi {
}, {
timeout: this.timeOut
})
if (res && res.res.statusCode === 200) {
res.prefixes && res.prefixes.forEach((item: string) => {
if (res?.res?.statusCode === 200) {
res?.prefixes?.forEach((item: string) => {
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))
})
window.webContents.send('refreshFileTransferList', result)
@@ -329,16 +327,10 @@ class AliyunApi {
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, marker, itemsPerPage } = configMap
const slicedPrefix = prefix.slice(1)
const urlPrefix = configMap.customUrl || `https://${bucket}.${region}.aliyuncs.com`
let res = {} as any
const result = {
fullList: <any>[],
isTruncated: false,
nextMarker: '',
success: false
}
const client = this.getNewCtx(region, bucket)
res = await client.listV2({
prefix: slicedPrefix === '' ? undefined : slicedPrefix,
const res = await client.listV2({
prefix: slicedPrefix || undefined,
delimiter: '/',
'max-keys': itemsPerPage.toString(),
'continuation-token': marker
@@ -347,18 +339,24 @@ class AliyunApi {
}) as any
// prefixes can be null
// objects will be [] when no file
if (res && res.res.statusCode === 200) {
res.prefixes && res.prefixes.forEach((item: string) => {
result.fullList.push(this.formatFolder(item, slicedPrefix))
})
res.objects && res.objects.forEach((item: OSS.ObjectMeta) => {
item.size !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
})
result.isTruncated = res.isTruncated
result.nextMarker = res.nextContinuationToken || ''
result.success = true
if (res?.res.statusCode !== 200) {
return {
fullList: [],
isTruncated: false,
nextMarker: '',
success: false
}
}
const fullList = [
...(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> {
const { bucketName, region, oldKey, newKey } = configMap
const client = this.getNewCtx(region, bucketName)
const res = await client.copy(
const copyRes = await client.copy(
newKey,
oldKey
) as any
if (res && res.res.statusCode === 200) {
const res2 = await client.delete(oldKey) as any
return res2 && res2.res.statusCode === 204
} else {
return false
if (copyRes?.res.statusCode === 200) {
const deleteRes = await client.delete(oldKey) as any
return deleteRes?.res.statusCode === 204
}
return false
}
/**
@@ -399,7 +396,7 @@ class AliyunApi {
const { bucketName, region, key } = configMap
const client = this.getNewCtx(region, bucketName)
const res = await client.delete(key) as any
return res && res.res.statusCode === 204
return res?.res.statusCode === 204
}
/**
@@ -415,62 +412,39 @@ class AliyunApi {
CommonPrefixes: [] as any[],
Contents: [] as any[]
}
let res = await client.listV2({
prefix: key,
delimiter: '/',
'max-keys': '1000'
}, {
timeout: this.timeOut
}) as any
if (res && res.res.statusCode === 200) {
do {
const res = await client.listV2({
prefix: key,
delimiter: '/',
'max-keys': '1000',
'continuation-token': marker
}, {
timeout: this.timeOut
}) as any
if (res?.res.statusCode !== 200) return false
res.prefixes !== null && allFileList.CommonPrefixes.push(...res.prefixes)
res.objects.length > 0 && allFileList.Contents.push(...res.objects)
res.objects?.length > 0 && allFileList.Contents.push(...res.objects)
isTruncated = res.isTruncated
marker = res.nextContinuationToken
while (isTruncated) {
res = await client.listV2({
prefix: key,
delimiter: '/',
'max-keys': '1000',
'continuation-token': marker
}, {
timeout: this.timeOut
}) as any
if (res && res.res.statusCode === 200) {
res.prefixes !== null && allFileList.CommonPrefixes.push(...res.prefixes)
res.objects.length > 0 && allFileList.Contents.push(...res.objects)
isTruncated = res.isTruncated
marker = res.nextContinuationToken
} else {
return false
}
}
} else {
return false
}
} while (isTruncated)
if (allFileList.CommonPrefixes.length > 0) {
for (const item of allFileList.CommonPrefixes) {
res = await this.deleteBucketFolder({
const successfully = await this.deleteBucketFolder({
bucketName,
region,
key: item
})
if (!res) {
return false
}
if (!successfully) return false
}
}
if (allFileList.Contents.length > 0) {
const cycle = Math.ceil(allFileList.Contents.length / 1000)
for (let i = 0; i < cycle; i++) {
res = await client.deleteMulti(
allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => {
return item.name
})
) as any
if (!(res && res.res.statusCode === 200)) {
return false
}
const deleteRes = await client.deleteMulti(
allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => item.name)) as any
if (deleteRes?.res.statusCode !== 200) return false
}
}
return true
@@ -493,7 +467,7 @@ class AliyunApi {
const res = client.signatureUrl(key, {
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) => {
const id = `${bucketName}-${region}-${key}-${filePath}`
if (res && res.res.statusCode === 200) {
if (res?.res?.statusCode === 200) {
instance.updateUploadTask({
id,
progress: 100,
@@ -587,7 +561,7 @@ class AliyunApi {
const { bucketName, region, key } = configMap
const client = this.getNewCtx(region, bucketName)
const res = await client.put(key, Buffer.from('')) as any
return res && res.res.statusCode === 200
return res?.res?.statusCode === 200
}
/**
+12 -8
View File
@@ -1,21 +1,25 @@
import TcyunApi from './tcyun'
import AliyunApi from './aliyun'
import QiniuApi from './qiniu'
import UpyunApi from './upyun'
import SmmsApi from './smms'
import GithubApi from './github'
import ImgurApi from './imgur'
import LocalApi from './local'
import QiniuApi from './qiniu'
import S3plistApi from './s3plist'
import SftpApi from './sftp'
import SmmsApi from './smms'
import TcyunApi from './tcyun'
import UpyunApi from './upyun'
import WebdavplistApi from './webdavplist'
export default {
TcyunApi,
AliyunApi,
QiniuApi,
UpyunApi,
SmmsApi,
GithubApi,
ImgurApi,
LocalApi,
QiniuApi,
S3plistApi,
SftpApi,
SmmsApi,
TcyunApi,
UpyunApi,
WebdavplistApi
}
+54 -66
View File
@@ -1,16 +1,34 @@
// HTTP 请求库
import got from 'got'
// 日志记录器
import { ManageLogger } from '../utils/logger'
// HTTP 代理格式化函数、是否为图片的判断函数
import { formatHttpProxy, isImage } from '~/renderer/manage/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron'
// got 上传函数、路径处理函数、新的下载器、获取请求代理、获取请求选项、并发异步任务池、错误格式化函数
import { gotUpload, trimPath, NewDownloader, getAgent, getOptions, ConcurrencyPromisePool, formatError } from '../utils/common'
import UpDownTaskQueue,
{
commonTaskStatus
} from '../datastore/upDownTaskQueue'
// 上传下载任务队列
import UpDownTaskQueue, { commonTaskStatus } from '../datastore/upDownTaskQueue'
// 文件系统库
import fs from 'fs-extra'
// 路径处理库
import path from 'path'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class GithubApi {
@@ -35,12 +53,7 @@ class GithubApi {
}
formatFolder (item: any, slicedPrefix: string) {
let key = ''
if (slicedPrefix === '') {
key = `${item.path}/`
} else {
key = `${slicedPrefix}/${item.path}/`
}
const key = `${slicedPrefix ? `${slicedPrefix}/` : ''}${item.path}/`
return {
...item,
Key: key,
@@ -57,27 +70,18 @@ class GithubApi {
formatFile (item: any, slicedPrefix: string, branch: string, repo: string, cdnUrl: string | undefined) {
let rawUrl = ''
if (cdnUrl) {
const placeholder = ['{username}', '{repo}', '{branch}', '{path}']
if (placeholder.some(item => cdnUrl.includes(item))) {
rawUrl = cdnUrl.replace('{username}', this.username)
.replace('{repo}', repo)
.replace('{branch}', branch)
.replace('{path}', `${slicedPrefix}/${item.path}`)
} else {
rawUrl = `${cdnUrl}/${slicedPrefix}/${item.path}`
}
} else {
rawUrl = `https://raw.githubusercontent.com/${this.username}/${repo}/${branch}/${slicedPrefix}/${item.path}`
}
const placeholders = ['{username}', '{repo}', '{branch}', '{path}']
const key = slicedPrefix === '' ? item.path : `${slicedPrefix}/${item.path}`
rawUrl = cdnUrl
? placeholders.some(item => cdnUrl.includes(item))
? placeholders.reduce((url, ph) => {
const value = ph === '{username}' ? this.username : ph === '{repo}' ? repo : ph === '{branch}' ? branch : ph === '{path}' ? `${slicedPrefix}/${item.path}` : ''
return url.replaceAll(ph, value)
}, cdnUrl)
: `${cdnUrl}/${key}`
: `https://raw.githubusercontent.com/${this.username}/${repo}/${branch}/${key}`
rawUrl = rawUrl.replace(/(?<!https?:)\/{2,}/g, '/')
let key = ''
if (slicedPrefix === '') {
key = item.path
} else {
key = `${slicedPrefix}/${item.path}`
}
const result = {
return {
...item,
Key: key,
key,
@@ -88,12 +92,9 @@ class GithubApi {
checked: false,
match: false,
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> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const { bucketName: repo, customUrl: branch, prefix, cancelToken, cdnUrl } = configMap
const slicedPrefix = prefix.replace(/^\//, '').replace(/\/$/, '')
const slicedPrefix = prefix.replace(/(^\/+|\/+$)/g, '')
const cancelTask = [false]
ipcMain.on(cancelDownloadLoadingFileList, (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) {
@@ -202,7 +203,7 @@ class GithubApi {
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const { bucketName: repo, customUrl: branch, prefix, cancelToken, cdnUrl } = configMap
const slicedPrefix = prefix.replace(/^\//, '').replace(/\/$/, '')
const slicedPrefix = prefix.replace(/(^\/+|\/+$)/g, '')
const cancelTask = [false]
ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) {
@@ -269,37 +270,35 @@ class GithubApi {
*/
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName: repo, githubBranch: branch, key } = configMap
// get sha of the branch
const refRes = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/git/refs/heads/${branch}`,
getOptions('GET', this.commonHeaders, undefined, 'json', undefined, undefined, this.proxy)
) as any
if (refRes.statusCode !== 200) {
return false
}
if (refRes.statusCode !== 200) return false
const refSha = refRes.body.object.sha
// get sha of the root tree
const rootRes = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/branches/${branch}`,
getOptions('GET', undefined, undefined, 'json', undefined, undefined, this.proxy)
) as any
if (rootRes.statusCode !== 200) {
return false
}
if (rootRes.statusCode !== 200) return false
const rootSha = rootRes.body.commit.commit.tree.sha
// TODO: if there are more than 10000 files in the folder, it will be truncated
// Rare cases, not considered for now
// get sha of the folder tree
const treeRes = await got(
`${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, {
recursive: true
}, 'json', undefined, undefined, this.proxy)
) as any
if (treeRes.statusCode !== 200) {
return false
}
if (treeRes.statusCode !== 200) return false
const oldTree = treeRes.body.tree
// create a new tree
const newTree = oldTree.filter((item: any) => item.type === 'blob')
.map((item:any) => ({
path: `${key.replace(/^\//, '').replace(/\/$/, '')}/${item.path}`,
path: `${key.replace(/(^\/+|\/+$)/g, '')}/${item.path}`,
mode: item.mode,
type: item.type,
sha: null
@@ -311,10 +310,9 @@ class GithubApi {
tree: newTree
}), undefined, this.proxy)
) as any
if (newTreeShaRes.statusCode !== 201) {
return false
}
if (newTreeShaRes.statusCode !== 201) return false
const newTreeSha = newTreeShaRes.body.sha
// create a new commit
const commitRes = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/git/commits`,
getOptions('POST', this.commonHeaders, undefined, 'json', JSON.stringify({
@@ -323,20 +321,16 @@ class GithubApi {
parents: [refSha]
}), undefined, this.proxy)
) as any
if (commitRes.statusCode !== 201) {
return false
}
if (commitRes.statusCode !== 201) return false
const commitSha = commitRes.body.sha
// update the branch
const updateRefRes = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/git/refs/heads/${branch}`,
getOptions('PATCH', this.commonHeaders, undefined, 'json', JSON.stringify({
sha: commitSha
}), undefined, this.proxy)
) as any
if (updateRefRes.statusCode !== 200) {
return false
}
return true
return updateRefRes.statusCode === 200
}
/**
@@ -352,20 +346,14 @@ class GithubApi {
*/
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
const { bucketName: repo, customUrl: branch, key, rawUrl, githubPrivate: isPrivate } = configMap
if (!isPrivate) {
return rawUrl
}
if (!isPrivate) return rawUrl
const res = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/contents/${key}`,
getOptions('GET', this.commonHeaders, {
ref: branch
}, 'json', undefined, undefined, this.proxy)
) as any
if (res.statusCode === 200) {
return res.body.download_url
} else {
return ''
}
return res.statusCode === 200 ? res.body.download_url : ''
}
/**
+34 -26
View File
@@ -1,17 +1,27 @@
import got from 'got'
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'
// External dependencies
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 {
userName: string
@@ -35,17 +45,19 @@ class ImgurApi {
}
formatFile (item: any) {
const fileName = path.basename(item.link)
const isImg = isImage(fileName)
return {
...item,
Key: path.basename(item.link),
key: path.basename(item.link),
Key: fileName,
key: fileName,
fileName: `${item.name}${path.extname(item.link)}`,
formatedTime: new Date(item.datetime * 1000).toLocaleString(),
fileSize: item.size,
isDir: false,
checked: false,
match: false,
isImage: isImage(path.basename(item.link)),
isImage: isImg,
url: item.link,
sha: item.deletehash
}
@@ -69,16 +81,12 @@ class ImgurApi {
result.push(...res.body.data)
initPage++
} while (res.body.data.length > 0)
const finalResult = [] as any[]
for (let i = 0; i < result.length; i++) {
const item = result[i]
finalResult.push({
...item,
Name: item.title,
Location: item.id,
CreationDate: item.datetime
})
}
const finalResult = result.map((item: any) => ({
...item,
Name: item.title,
Location: item.id,
CreationDate: item.datetime
})) as any[]
finalResult.push({
Name: '全部',
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'
// 加密函数、获取文件 MIME 类型、新的下载器、错误格式化函数、并发异步任务池
import { hmacSha1Base64, getFileMimeType, NewDownloader, formatError, ConcurrencyPromisePool } from '../utils/common'
// 七牛云客户端库
import qiniu from 'qiniu/index'
// 路径处理库
import path from 'path'
// 是否为图片的判断函数
import { isImage } from '~/renderer/manage/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron'
import UpDownTaskQueue,
{
uploadTaskSpecialStatus,
commonTaskStatus
} from '../datastore/upDownTaskQueue'
// 上传下载任务队列
import UpDownTaskQueue, { uploadTaskSpecialStatus, commonTaskStatus } from '../datastore/upDownTaskQueue'
// 日志记录器
import { ManageLogger } from '../utils/logger'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class QiniuApi {
@@ -49,16 +66,17 @@ class QiniuApi {
}
formatFile (item: any, slicedPrefix: string, urlPrefix: string) {
const fileName = item.key.replace(slicedPrefix, '')
return {
...item,
fileName: item.key.replace(slicedPrefix, ''),
fileName,
url: `${urlPrefix}/${item.key}`,
fileSize: item.fsize,
formatedTime: new Date(parseInt(item.putTime.toString().slice(0, -4), 10)).toLocaleString(),
isDir: false,
checked: false,
match: false,
isImage: isImage(item.key.replace(slicedPrefix, ''))
isImage: isImage(fileName)
}
}
@@ -71,22 +89,20 @@ class QiniuApi {
contentType: string,
xQiniuHeaders?: IStringKeyMap
) {
let signStr = `${method.toUpperCase()} ${urlPath}`
query && (signStr += `?${query}`)
signStr += `\nHost: ${host}`
let signStr = `${method.toUpperCase()} ${urlPath}${query ? `?${query}` : ''}\nHost: ${host}`
contentType && (signStr += `\nContent-Type: ${contentType}`)
let xQiniuHeaderStr = ''
if (xQiniuHeaders) {
const xQiniuHeaderKeys = Object.keys(xQiniuHeaders).sort()
xQiniuHeaderKeys.forEach((key) => {
xQiniuHeaderStr += `\n${key}:${xQiniuHeaders[key]}`
})
const xQiniuHeaderStr = Object.keys(xQiniuHeaders)
.sort()
.map((key) => `\n${key}:${xQiniuHeaders[key]}`)
.join('')
signStr += xQiniuHeaderStr
}
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, '_')}`
}
@@ -103,28 +119,21 @@ class QiniuApi {
},
timeout: this.timeout
})
if (res && res.status === 200) {
if (res.data && res.data.length) {
const result = [] as any[]
for (let i = 0; i < res.data.length; i++) {
const info = await this.getBucketInfo({ bucketName: res.data[i] })
if (!info.success) {
return []
}
result.push({
Name: res.data[i],
Location: info.zone,
CreationDate: new Date().toISOString(),
Private: info.private
})
}
return result
} else {
return []
if (res?.status === 200 && res?.data?.length) {
const result = [] as any[]
for (let i = 0; i < res.data.length; i++) {
const info = await this.getBucketInfo({ bucketName: res.data[i] })
if (!info.success) return []
result.push({
Name: res.data[i],
Location: info.zone,
CreationDate: new Date().toISOString(),
Private: info.private
})
}
} else {
return []
return result
}
return []
}
/**
@@ -148,17 +157,15 @@ class QiniuApi {
},
timeout: this.timeout
})
if (res && res.status === 200) {
return {
return res?.status === 200
? {
success: true,
private: res.data.private,
zone: res.data.zone
}
} else {
return {
: {
success: false
}
}
}
/**
@@ -178,11 +185,7 @@ class QiniuApi {
},
timeout: this.timeout
})
if (res && res.status === 200) {
return res.data && res.data.length ? res.data : []
} else {
return []
}
return res?.status === 200 && res?.data?.length ? res.data : []
}
/**
@@ -209,7 +212,7 @@ class QiniuApi {
},
timeout: this.timeout
})
return res && res.status === 200
return res?.status === 200
}
/**
@@ -222,8 +225,7 @@ class QiniuApi {
* }
*/
async createBucket (configMap: IStringKeyMap): Promise<boolean> {
const { BucketName, region } = configMap
const { acl } = configMap
const { BucketName, region, acl } = configMap
const urlPath = `/mkbucketv3/${BucketName}/region/${region}`
const authorization = this.authorization('POST', urlPath, this.host, '', '', 'application/json')
const res = await axios({
@@ -236,15 +238,12 @@ class QiniuApi {
},
timeout: this.timeout
})
if (res && res.status === 200) {
const changeAclRes = await this.setBucketAclPolicy({
return res?.status === 200
? await this.setBucketAclPolicy({
bucketName: BucketName,
isPrivate: !acl
})
return changeAclRes
} else {
return false
}
: false
}
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
@@ -407,19 +406,19 @@ class QiniuApi {
}
})
})
if (res && res.respInfo.statusCode === 200) {
if (res.respBody && res.respBody.commonPrefixes) {
if (res?.respInfo?.statusCode === 200) {
if (res.respBody?.commonPrefixes) {
res.respBody.commonPrefixes.forEach((item: string) => {
result.fullList.push(this.formatFolder(item, slicedPrefix))
})
}
if (res.respBody && res.respBody.items) {
if (res.respBody?.items) {
res.respBody.items.forEach((item: any) => {
item.fsize !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
})
}
result.isTruncated = !!(res.respBody && res.respBody.marker)
result.nextMarker = res.respBody && res.respBody.marker ? res.respBody.marker : ''
result.isTruncated = !!(res.respBody?.marker)
result.nextMarker = res.respBody?.marker ? res.respBody.marker : ''
result.success = true
}
return result
@@ -450,11 +449,7 @@ class QiniuApi {
}
})
}) as any
if (res && res.respInfo.statusCode === 200) {
return true
} else {
return false
}
return res?.respInfo?.statusCode === 200
}
/**
@@ -487,12 +482,12 @@ class QiniuApi {
}
})
}) as any
if (res && res.respInfo.statusCode === 200) {
if (res.respBody && res.respBody.items) {
if (res?.respInfo?.statusCode === 200) {
if (res.respBody?.items) {
allFileList.Contents = allFileList.Contents.concat(res.respBody.items)
}
isTruncated = !!(res.respBody && res.respBody.marker)
marker = res.respBody && res.respBody.marker ? res.respBody.marker : ''
isTruncated = !!(res.respBody?.marker)
marker = res.respBody?.marker ? res.respBody.marker : ''
} else {
return false
}
@@ -514,9 +509,7 @@ class QiniuApi {
}
})
}) as any
if (!(res && res.respInfo.statusCode === 200)) {
return false
}
if (res?.respInfo?.statusCode !== 200) return false
}
return true
}
@@ -549,7 +542,7 @@ class QiniuApi {
}
})
}) as any
return res && res.respInfo.statusCode === 200
return res?.respInfo?.statusCode === 200
}
/**
@@ -671,11 +664,7 @@ class QiniuApi {
}
})
}) as any
if (res && res.respInfo.statusCode === 200) {
return true
} else {
return false
}
return res?.respInfo?.statusCode === 200
}
/**
+182 -107
View File
@@ -1,9 +1,9 @@
// AWS S3 相关
import {
S3Client,
ListBucketsCommand,
ListObjectsV2Command,
GetBucketLocationCommand,
S3ClientConfig,
_Object,
CommonPrefix,
ListObjectsV2CommandOutput,
@@ -11,46 +11,63 @@ import {
GetObjectCommand,
DeleteObjectCommand,
DeleteObjectsCommand,
PutObjectCommand
PutObjectCommand,
S3ClientConfig
} from '@aws-sdk/client-s3'
// AWS S3 上传和进度
import { Upload, Progress } from '@aws-sdk/lib-storage'
// AWS S3 请求签名
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
// HTTP 和 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'
// 端点地址格式化函数、错误格式化函数、获取请求代理、获取文件 MIME 类型、新的下载器、并发异步任务池
import { formatEndpoint, formatError, getAgent, getFileMimeType, NewDownloader, ConcurrencyPromisePool } from '../utils/common'
// 是否为图片的判断函数、HTTP 代理格式化函数
import { isImage, formatHttpProxy } from '@/manage/utils/common'
import { HttpsProxyAgent, HttpProxyAgent } from 'hpagent'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron'
import UpDownTaskQueue,
{
uploadTaskSpecialStatus,
commonTaskStatus
} from '../datastore/upDownTaskQueue'
// 上传下载任务队列
import UpDownTaskQueue, { uploadTaskSpecialStatus, commonTaskStatus } from '../datastore/upDownTaskQueue'
// 文件系统库
import fs from 'fs-extra'
// 路径处理库
import path from 'path'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
interface S3plistApiOptions {
credentials: {
accessKeyId: string
secretAccessKey: string
}
endpoint?: string
sslEnabled: boolean
s3ForcePathStyle: boolean
httpOptions?: {
agent: https.Agent
}
}
// dogecloudApi
import { dogecloudApi, DogecloudToken, getTempToken } from '../utils/dogeAPI'
class S3plistApi {
baseOptions: S3plistApiOptions
baseOptions: S3ClientConfig
logger: ManageLogger
agent: any
proxy: string | undefined
dogeCloudSupport: boolean
accessKeyId: string
secretAccessKey: string
bucketName: string
constructor (
accessKeyId: string,
@@ -59,38 +76,66 @@ class S3plistApi {
sslEnabled: boolean,
s3ForcePathStyle: boolean,
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 = {
credentials: {
accessKeyId,
secretAccessKey
},
endpoint: endpoint ? formatEndpoint(endpoint, sslEnabled) : undefined,
sslEnabled,
s3ForcePathStyle,
httpOptions: {
agent: this.setAgent(proxy, sslEnabled)
}
} as S3plistApiOptions
tls: sslEnabled,
forcePathStyle: s3ForcePathStyle,
requestHandler: this.setAgent(proxy, sslEnabled)
}
this.logger = logger
this.agent = this.setAgent(proxy, sslEnabled)
this.proxy = formatHttpProxy(proxy, 'string') as string | undefined
}
setAgent (proxy: string | undefined, sslEnabled: boolean) : HttpProxyAgent | HttpsProxyAgent | undefined {
if (sslEnabled) {
const agent = getAgent(proxy, true).https
return agent ?? new https.Agent({
keepAlive: true,
rejectUnauthorized: false
})
} else {
const agent = getAgent(proxy, false).http
return agent ?? new http.Agent({
keepAlive: true
})
async getDogeCloudToken () {
if (!this.dogeCloudSupport) return
const token = await getTempToken(this.accessKeyId, this.secretAccessKey) as DogecloudToken
if (Object.keys(token).length === 0) {
throw new Error('manage.setting.dogeCloudTokenError')
}
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) =>
@@ -111,17 +156,18 @@ class S3plistApi {
}
formatFile (item: _Object, slicedPrefix: string, urlPrefix: string): any {
const fileName = item.Key?.replace(slicedPrefix, '')
return {
...item,
key: item.Key,
url: `${urlPrefix}/${item.Key}`,
fileName: item.Key?.replace(slicedPrefix, ''),
fileName,
fileSize: item.Size,
formatedTime: new Date(item.LastModified!).toLocaleString(),
isDir: false,
checked: false,
match: false,
isImage: isImage(item.Key?.replace(slicedPrefix, '') || '')
isImage: isImage(fileName || '')
}
}
@@ -129,51 +175,64 @@ class S3plistApi {
* 获取存储桶列表
*/
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 result = [] as IStringKeyMap[]
const endpoint = options.endpoint as string || '' as string
options.region = endpoint.indexOf('cloudflarestorage') !== -1 ? 'auto' : 'us-east-1'
const result: IStringKeyMap[] = []
const endpoint = options.endpoint as string || ''
options.region = endpoint.includes('cloudflarestorage') ? 'auto' : 'us-east-1'
try {
const client = new S3Client(options)
const command = new ListBucketsCommand({})
const data = await client.send(command)
if (data.$metadata.httpStatusCode === 200) {
if (data.Buckets) {
if (endpoint.indexOf('cloudflarestorage') !== -1) {
data.Buckets.forEach((bucket) => {
result.push({
Name: bucket.Name,
CreationDate: bucket.CreationDate,
Location: 'auto'
})
const data = await client.send(new ListBucketsCommand({}))
if (data.$metadata.httpStatusCode !== 200) {
this.logParam(data, 'getBucketList')
return result
}
if (data.Buckets) {
if (endpoint.includes('cloudflarestorage')) {
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 {
for (let i = 0; i < data.Buckets.length; i++) {
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'
})
}
if (bucketConfig.$metadata.httpStatusCode !== 200) {
this.logParam(bucketConfig, 'getBucketList')
}
}
}
} else {
this.logParam(data, 'getBucketList')
}
} catch (error) {
this.logParam(error, 'getBucketList')
@@ -203,7 +262,7 @@ class S3plistApi {
try {
do {
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 command = new ListObjectsV2Command({
Bucket: bucket,
@@ -259,9 +318,10 @@ class S3plistApi {
finished: false
}
try {
await this.getDogeCloudToken()
do {
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 command = new ListObjectsV2Command({
Bucket: bucket,
@@ -312,8 +372,8 @@ class S3plistApi {
success: false
}
try {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1'
await this.getDogeCloudToken()
const options = Object.assign({}, { ...this.baseOptions, region: String(region) || 'us-east-1' }) as S3ClientConfig
const client = new S3Client(options)
const command = new ListObjectsV2Command({
Bucket: bucket,
@@ -324,12 +384,10 @@ class S3plistApi {
})
const data = await client.send(command)
if (data.$metadata.httpStatusCode === 200) {
data.CommonPrefixes && data.CommonPrefixes.forEach((item: CommonPrefix) => {
result.fullList.push(this.formatFolder(item, slicedPrefix))
})
data.Contents && data.Contents.forEach((item: _Object) => {
result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
})
result.fullList = [
...(data.CommonPrefixes?.map(item => this.formatFolder(item, slicedPrefix)) || []),
...(data.Contents?.map(item => this.formatFile(item, slicedPrefix, urlPrefix)) || [])
]
result.isTruncated = data.IsTruncated || false
result.nextMarker = data.NextContinuationToken || ''
result.success = true
@@ -354,8 +412,8 @@ class S3plistApi {
const { bucketName, region, oldKey, newKey } = configMap
let result = false
try {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1'
await this.getDogeCloudToken()
const options = Object.assign({}, { ...this.baseOptions, region: String(region) || 'us-east-1' }) as S3ClientConfig
const client = new S3Client(options)
const command = new CopyObjectCommand({
Bucket: bucketName,
@@ -396,8 +454,9 @@ class S3plistApi {
const { bucketName, region, key } = configMap
let result = false
try {
await this.getDogeCloudToken()
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 command = new DeleteObjectCommand({
Bucket: bucketName,
@@ -430,9 +489,10 @@ class S3plistApi {
Contents: [] as any[]
}
try {
await this.getDogeCloudToken()
do {
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 command = new ListObjectsV2Command({
Bucket: bucketName,
@@ -467,7 +527,7 @@ class S3plistApi {
if (allFileList.Contents.length > 0) {
const cycle = Math.ceil(allFileList.Contents.length / 1000)
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)
for (let i = 0; i < cycle; i++) {
const deleteList = allFileList.Contents.slice(i * 1000, (i + 1) * 1000)
@@ -510,8 +570,9 @@ class S3plistApi {
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
const { bucketName, region, key, expires } = configMap
try {
await this.getDogeCloudToken()
const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1'
options.region = String(region) || 'us-east-1'
const client = new S3Client(options)
const signedUrl = await getSignedUrl(client, new GetObjectCommand({
Bucket: bucketName,
@@ -534,8 +595,9 @@ class S3plistApi {
const { bucketName, region, key } = configMap
let result = false
try {
await this.getDogeCloudToken()
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 command = new PutObjectCommand({
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']
for (const item of fileArray) {
const { bucketName, region, key, filePath, fileName, aclForUpload } = item
const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1'
const client = new S3Client(options)
const id = `${bucketName}-${region}-${key}-${filePath}`
const id = `${bucketName}-${String(region)}-${key}-${filePath}`
if (instance.getUploadTask(id)) {
continue
}
const fileStream = fs.createReadStream(filePath)
instance.addUploadTask({
id,
progress: 0,
@@ -589,8 +647,25 @@ class S3plistApi {
sourceFilePath: filePath,
targetFilePath: key,
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({
client,
params: {
@@ -652,7 +727,7 @@ class S3plistApi {
for (const item of fileArray) {
const { bucketName, region, key, fileName, customUrl } = item
const savedFilePath = path.join(downloadPath, fileName)
const id = `${bucketName}-${region}-${key}-${savedFilePath}`
const id = `${bucketName}-${String(region)}-${key}-${savedFilePath}`
if (instance.getDownloadTask(id)) {
continue
}
@@ -665,7 +740,7 @@ class S3plistApi {
})
const preSignedUrl = await this.getPreSignedUrl({
bucketName,
region,
region: String(region),
key,
expires: 36000,
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'
// Axios 和 Axios 实例类型声明
import axios, { AxiosInstance } from 'axios'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron'
// 表单数据库
import FormData from 'form-data'
// 文件系统库
import fs from 'fs-extra'
// 获取文件 MIME 类型、got 上传函数、新的下载器、并发异步任务池、错误格式化函数
import { getFileMimeType, gotUpload, NewDownloader, ConcurrencyPromisePool, formatError } from '../utils/common'
// 路径处理库
import path from 'path'
// 上传下载任务队列
import UpDownTaskQueue, { commonTaskStatus } from '../datastore/upDownTaskQueue'
// 日志记录器
import { ManageLogger } from '../utils/logger'
class SmmsApi {
@@ -99,7 +120,7 @@ class SmmsApi {
return
}
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.finished = true
window.webContents.send('refreshFileTransferList', result)
@@ -121,16 +142,14 @@ class SmmsApi {
* customUrl: string
* }
*/
async getBucketFileList (configMap: IStringKeyMap): Promise<any> {
const { currentPage } = configMap
let res = {} as any
async getBucketFileList ({ currentPage }: IStringKeyMap): Promise<any> {
const result = {
fullList: <any>[],
isTruncated: false,
nextMarker: '',
success: false
}
res = await this.axiosInstance(
const res = await this.axiosInstance(
'/upload_history',
{
method: 'GET',
@@ -142,21 +161,17 @@ class SmmsApi {
}
}
)
if (res && res.status === 200 && res.data && res.data.success) {
if (res.data.Count === 0) {
result.success = true
return result
}
res.data.data.forEach((item: any) => {
result.fullList.push(this.formatFile(item))
})
result.isTruncated = res.data.CurrentPage < res.data.TotalPages
result.nextMarker = res.data.CurrentPage + 1
result.success = true
return result
} else {
return result
}
if (res?.status !== 200 || !res?.data?.success) return result
if (res.data.Count === 0) return { ...result, success: true }
res.data.data.forEach((item: any) => {
result.fullList.push(this.formatFile(item))
})
result.isTruncated = res.data.CurrentPage < res.data.TotalPages
result.nextMarker = res.data.CurrentPage + 1
result.success = true
return result
}
/**
@@ -169,20 +184,18 @@ class SmmsApi {
* DeleteHash: string
* }
*/
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { DeleteHash } = configMap
const params = {
hash: DeleteHash,
format: 'json'
}
async deleteBucketFile ({ DeleteHash }: IStringKeyMap): Promise<boolean> {
const res = await this.axiosInstance(
`/delete/${DeleteHash}`,
{
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 fs from 'fs-extra'
// 路径处理库
import path from 'path'
// 是否为图片的判断函数
import { isImage } from '~/renderer/manage/utils/common'
// URL 编码处理函数
import { handleUrlEncode } from '~/universal/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron'
// 错误格式化函数、获取文件 MIME 类型
import { formatError, getFileMimeType } from '../utils/common'
import UpDownTaskQueue,
{
uploadTaskSpecialStatus,
commonTaskStatus,
downloadTaskSpecialStatus
} from '../datastore/upDownTaskQueue'
// 上传下载任务队列
import UpDownTaskQueue, { uploadTaskSpecialStatus, commonTaskStatus, downloadTaskSpecialStatus } from '../datastore/upDownTaskQueue'
// 日志记录器
import { ManageLogger } from '../utils/logger'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class TcyunApi {
@@ -62,7 +80,7 @@ class TcyunApi {
*/
async getBucketList (): Promise<any> {
const res = await this.ctx.getService({})
return res && res.Buckets ? res.Buckets : []
return res?.Buckets || []
}
/**
@@ -74,21 +92,8 @@ class TcyunApi {
Bucket: bucketName,
Region: region
})
const result = [] as string[]
if (res && res.statusCode === 200) {
if (res.DomainRule && res.DomainRule.length > 0) {
res.DomainRule.forEach((item: any) => {
if (item.Status === 'ENABLED') {
result.push(item.Name)
}
})
return result
} else {
return []
}
} else {
return []
}
if (res?.statusCode !== 200 || !res?.DomainRule?.length) return []
return res.DomainRule.filter((item: any) => item.Status === 'ENABLED').map(item => item.Name)
}
/**
@@ -113,31 +118,29 @@ class TcyunApi {
Bucket: configMap.BucketName,
Region: configMap.region
})
return res && res.statusCode === 200
return res?.statusCode === 200
}
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const bucket = configMap.bucketName
const region = configMap.bucketConfig.Location
const prefix = configMap.prefix as string
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, customUrl, cancelToken } = configMap
const slicedPrefix = prefix.slice(1, prefix.length)
const urlPrefix = configMap.customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
let marker
const cancelToken = configMap.cancelToken as string
const urlPrefix = customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
const cancelTask = [false]
let marker
ipcMain.on(cancelDownloadLoadingFileList, (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) {
cancelTask[0] = true
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
}
})
let res = {} as COS.GetBucketResult
const result = {
fullList: <any>[],
success: false,
finished: false
}
let res = {} as COS.GetBucketResult
do {
res = await this.ctx.getBucket({
Bucket: bucket,
@@ -145,9 +148,9 @@ class TcyunApi {
Prefix: slicedPrefix === '' ? undefined : slicedPrefix,
Marker: marker
})
if (res && res.statusCode === 200) {
res.Contents.forEach((item: COS.CosObject) =>
parseInt(item.Size) !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)))
if (res?.statusCode === 200) {
result.fullList.push(...res.Contents.filter(item => parseInt(item.Size) !== 0)
.map(item => this.formatFile(item, slicedPrefix, urlPrefix)))
window.webContents.send(refreshDownloadFileTransferList, result)
} else {
result.finished = true
@@ -165,14 +168,12 @@ class TcyunApi {
async getBucketListBackstage (configMap: IStringKeyMap): Promise < any > {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const bucket = configMap.bucketName
const region = configMap.bucketConfig.Location
const prefix = configMap.prefix as string
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, customUrl, cancelToken } = configMap
const slicedPrefix = prefix.slice(1, prefix.length)
const urlPrefix = configMap.customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
let marker
const cancelToken = configMap.cancelToken as string
const urlPrefix = customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
const cancelTask = [false]
let marker
ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) {
cancelTask[0] = true
@@ -193,11 +194,12 @@ class TcyunApi {
Delimiter: '/',
Marker: marker
})
if (res && res.statusCode === 200) {
res.CommonPrefixes.forEach((item: { Prefix: string}) =>
result.fullList.push(this.formatFolder(item, slicedPrefix)))
res.Contents.forEach((item: COS.CosObject) =>
parseInt(item.Size) !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)))
if (res?.statusCode === 200) {
result.fullList.push(
...res.CommonPrefixes.map(item => this.formatFolder(item, slicedPrefix)),
...res.Contents.filter(item => parseInt(item.Size) !== 0)
.map(item => this.formatFile(item, slicedPrefix, urlPrefix))
)
window.webContents.send('refreshFileTransferList', result)
} else {
result.finished = true
@@ -229,36 +231,34 @@ class TcyunApi {
* }
*/
async getBucketFileList (configMap: IStringKeyMap): Promise<any> {
const bucket = configMap.bucketName
const region = configMap.bucketConfig.Location
const prefix = configMap.prefix as string
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, customUrl, marker, itemsPerPage } = configMap
const slicedPrefix = prefix.slice(1)
const urlPrefix = configMap.customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
const marker = configMap.marker as string
const itemsPerPage = configMap.itemsPerPage as number
let res = {} as COS.GetBucketResult
const result = {
fullList: <any>[],
isTruncated: false,
nextMarker: '',
success: false
}
res = await this.ctx.getBucket({
const urlPrefix = customUrl || `https://${bucket}.cos.${region}.myqcloud.com`
const res = await this.ctx.getBucket({
Bucket: bucket,
Region: region,
Prefix: slicedPrefix === '' ? undefined : slicedPrefix,
Delimiter: '/',
Marker: marker,
MaxKeys: itemsPerPage
})
if (res && res.statusCode === 200) {
res.CommonPrefixes.forEach((item: { Prefix: string}) =>
result.fullList.push(this.formatFolder(item, slicedPrefix)))
res.Contents.forEach((item: COS.CosObject) =>
parseInt(item.Size) !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)))
result.isTruncated = res.IsTruncated === 'true'
result.nextMarker = res.NextMarker || ''
result.success = true
}) as COS.GetBucketResult
if (res?.statusCode !== 200) {
return {
fullList: [],
isTruncated: false,
nextMarker: '',
success: false
}
}
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
}
@@ -275,22 +275,22 @@ class TcyunApi {
*/
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName, region, oldKey, newKey } = configMap
const res = await this.ctx.putObjectCopy({
const copyRes = await this.ctx.putObjectCopy({
Bucket: bucketName,
Region: region,
Key: newKey,
CopySource: handleUrlEncode(`${bucketName}.cos.${region}.myqcloud.com/${oldKey}`)
})
if (res && res.statusCode === 200) {
const res2 = await this.ctx.deleteObject({
Bucket: bucketName,
Region: region,
Key: oldKey
})
return res2 && res2.statusCode === 204
} else {
return false
}
if (copyRes?.statusCode !== 200) return false
const deleteRes = await this.ctx.deleteObject({
Bucket: bucketName,
Region: region,
Key: oldKey
})
return deleteRes?.statusCode === 204
}
/**
@@ -309,7 +309,7 @@ class TcyunApi {
Region: region,
Key: key
})
return res && res.statusCode === 204
return res?.statusCode === 204
}
/**
@@ -319,72 +319,38 @@ class TcyunApi {
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName, region, key } = configMap
let marker
let isTruncated
let res: any
const allFileList = {
CommonPrefixes: [] as any[],
Contents: [] as any[]
}
let res = await this.ctx.getBucket({
Bucket: bucketName,
Region: region,
Prefix: key,
Delimiter: '/',
MaxKeys: 1000
})
if (res && res.statusCode === 200) {
res.CommonPrefixes.length > 0 && allFileList.CommonPrefixes.push(...res.CommonPrefixes)
res.Contents.length > 0 && allFileList.Contents.push(...res.Contents)
isTruncated = res.IsTruncated
do {
res = await this.ctx.getBucket({
Bucket: bucketName,
Region: region,
Prefix: key,
Delimiter: '/',
MaxKeys: 1000,
Marker: marker
})
if (res?.statusCode !== 200) return false
allFileList.CommonPrefixes.push(...res.CommonPrefixes)
allFileList.Contents.push(...res.Contents)
marker = res.NextMarker
while (isTruncated === 'true') {
res = await this.ctx.getBucket({
Bucket: bucketName,
Region: region,
Prefix: key,
Delimiter: '/',
Marker: marker,
MaxKeys: 1000
}) as any
if (res && res.statusCode === 200) {
res.CommonPrefixes.length > 0 && allFileList.CommonPrefixes.push(...res.CommonPrefixes)
res.Contents.length > 0 && allFileList.Contents.push(...res.Contents)
isTruncated = res.IsTruncated
marker = res.NextMarker
} else {
return false
}
}
} else {
return false
} while (res.IsTruncated === 'true')
for (const item of allFileList.CommonPrefixes) {
if (!(await this.deleteBucketFolder({ bucketName, region, key: item.Prefix }))) return false
}
if (allFileList.CommonPrefixes.length > 0) {
for (const item of allFileList.CommonPrefixes) {
res = await this.deleteBucketFolder({
bucketName,
region,
key: item.Prefix
}) as any
if (!res) {
return false
}
}
}
if (allFileList.Contents.length > 0) {
const cycle = Math.ceil(allFileList.Contents.length / 1000)
for (let i = 0; i < cycle; i++) {
res = await this.ctx.deleteMultipleObject({
Bucket: bucketName,
Region: region,
Objects: allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => {
return {
Key: item.Key
}
})
}) as any
if (!(res && res.statusCode === 200)) {
return false
}
}
const cycles = Math.ceil(allFileList.Contents.length / 1000)
for (let i = 0; i < cycles; i++) {
const res = await this.ctx.deleteMultipleObject({
Bucket: bucketName,
Region: region,
Objects: allFileList.Contents.slice(i * 1000, (i + 1) * 1000).map((item: any) => ({ Key: item.Key }))
})
if (res?.statusCode !== 200) return false
}
return true
}
@@ -410,7 +376,7 @@ class TcyunApi {
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,
Body: ''
})
return res && res.statusCode === 200
return res?.statusCode === 200
}
/**
+39 -23
View File
@@ -1,19 +1,41 @@
// 忽略 TypeScript 错误
// @ts-ignore
import Upyun from 'upyun'
// 加密函数、获取文件 MIME 类型、新的下载器、got 上传函数、并发异步任务池、错误格式化函数
import { md5, hmacSha1Base64, getFileMimeType, NewDownloader, gotUpload, ConcurrencyPromisePool, formatError } from '../utils/common'
// 是否为图片的判断函数
import { isImage } from '~/renderer/manage/utils/common'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron'
// Axios
import axios from 'axios'
// 表单数据库
import FormData from 'form-data'
// 文件系统库
import fs from 'fs-extra'
// 路径处理库
import path from 'path'
import UpDownTaskQueue,
{
commonTaskStatus
} from '../datastore/upDownTaskQueue'
// 上传下载任务队列
import UpDownTaskQueue, { commonTaskStatus } from '../datastore/upDownTaskQueue'
// 日志记录器
import { ManageLogger } from '../utils/logger'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class UpyunApi {
@@ -35,9 +57,10 @@ class UpyunApi {
}
formatFolder (item: any, slicedPrefix: string) {
const key = `${slicedPrefix}${item.name}/`
return {
...item,
key: `${slicedPrefix}${item.name}/`,
key,
fileSize: 0,
formatedTime: '',
fileName: item.name,
@@ -45,11 +68,12 @@ class UpyunApi {
checked: false,
isImage: false,
match: false,
Key: `${slicedPrefix}${item.name}/`
Key: key
}
}
formatFile (item: any, slicedPrefix: string, urlPrefix: string) {
const key = `${slicedPrefix}${item.name}`
return {
...item,
fileName: item.name,
@@ -59,8 +83,8 @@ class UpyunApi {
checked: false,
match: false,
isImage: isImage(item.name),
url: `${urlPrefix}/${slicedPrefix}${item.name}`,
key: `${slicedPrefix}${item.name}`
url: `${urlPrefix}/${key}`,
key
}
}
@@ -71,18 +95,10 @@ class UpyunApi {
operator: string,
password: string
) {
const passwordMd5 = md5(password, 'hex')
const date = new Date().toUTCString()
const upperMethod = method.toUpperCase()
let stringToSign = ''
const codedUri = encodeURI(uri)
if (contentMd5 === '') {
stringToSign = `${upperMethod}&${codedUri}&${date}`
} else {
stringToSign = `${upperMethod}&${codedUri}&${date}&${contentMd5}`
}
const signature = hmacSha1Base64(passwordMd5, stringToSign)
return `UPYUN ${operator}:${signature}`
return `UPYUN ${operator}:${hmacSha1Base64(
md5(password, 'hex'),
`${method.toUpperCase()}&${encodeURI(uri)}&${new Date().toUTCString()}${contentMd5 ? `&${contentMd5}` : ''}`
)}`
}
/**
@@ -120,7 +136,7 @@ class UpyunApi {
iter: marker
})
if (res) {
res.files && res.files.forEach((item: any) => {
res.files?.forEach((item: any) => {
item.type === 'F' && folderQueue.push(`${slicedPrefix}${item.name}/`)
item.type === 'N' && result.fullList.push(this.formatFile(item, folder, urlPrefix))
})
@@ -169,7 +185,7 @@ class UpyunApi {
iter: marker
})
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 === 'F' && result.fullList.push(this.formatFolder(item, slicedPrefix))
})
@@ -219,7 +235,7 @@ class UpyunApi {
iter: marker || ''
})
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 === 'F' && result.fullList.push(this.formatFolder(item, slicedPrefix))
})
+48 -29
View File
@@ -1,19 +1,38 @@
// 日志记录器
import ManageLogger from '../utils/logger'
// WebDAV 客户端库
import { createClient, WebDAVClient, FileStat, ProgressEvent } from 'webdav'
// 错误格式化函数、端点地址格式化函数、获取内部代理、新的下载器、并发异步任务池
import { formatError, formatEndpoint, getInnerAgent, NewDownloader, ConcurrencyPromisePool } from '../utils/common'
// HTTP 代理格式化函数、是否为图片的判断函数
import { formatHttpProxy, isImage } from '@/manage/utils/common'
// HTTP 和 HTTPS 模块
import http from 'http'
import https from 'https'
// 窗口管理器
import windowManager from 'apis/app/window/windowManager'
// 枚举类型声明
import { IWindowList } from '#/types/enum'
// Electron 相关
import { ipcMain, IpcMainEvent } from 'electron'
import UpDownTaskQueue,
{
uploadTaskSpecialStatus,
commonTaskStatus
} from '../datastore/upDownTaskQueue'
// 上传下载任务队列
import UpDownTaskQueue, { uploadTaskSpecialStatus, commonTaskStatus } from '../datastore/upDownTaskQueue'
// 文件系统库
import fs from 'fs-extra'
// 路径处理库
import path from 'path'
// 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class WebdavplistApi {
@@ -52,35 +71,37 @@ class WebdavplistApi {
logParam = (error:any, method: string) =>
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 {
...item,
key: item.filename.replace(/^\/+/, ''),
key,
fileName: item.basename,
fileSize: 0,
Key: item.filename.replace(/^\/+/, ''),
Key: key,
formatedTime: '',
isDir: true,
checked: false,
isImage: 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 {
...item,
key: item.filename.replace(/^\/+/, ''),
key,
fileName: item.basename,
fileSize: item.size,
Key: item.filename.replace(/^\/+/, ''),
Key: key,
formatedTime: new Date(item.lastmod).toLocaleString(),
isDir: false,
checked: false,
match: false,
isImage: isImage(item.basename),
url: `${urlPrefix}${item.filename}`
url: isWebPath ? urlPrefix : `${urlPrefix}${item.filename}`
}
}
@@ -109,27 +130,18 @@ class WebdavplistApi {
details: true
})
if (this.isRequestSuccess(res.status)) {
if (res.data && res.data.length) {
if (res.data?.length) {
res.data.forEach((item: FileStat) => {
if (item.type !== 'directory') {
result.fullList.push(this.formatFile(item, urlPrefix))
}
})
}
} else {
result.finished = true
window.webContents.send(refreshDownloadFileTransferList, result)
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
return
result.success = true
}
} catch (error) {
this.logParam(error, 'getBucketListRecursively')
result.finished = true
window.webContents.send(refreshDownloadFileTransferList, result)
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
return
}
result.success = true
result.finished = true
window.webContents.send(refreshDownloadFileTransferList, result)
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
@@ -137,8 +149,13 @@ class WebdavplistApi {
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const { prefix, customUrl, cancelToken } = configMap
const urlPrefix = customUrl || this.endpoint
const { prefix, customUrl, cancelToken, baseDir } = configMap
let urlPrefix = customUrl || this.endpoint
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) {
@@ -158,12 +175,14 @@ class WebdavplistApi {
details: true
})
if (this.isRequestSuccess(res.status)) {
if (res.data && res.data.length) {
if (res.data?.length) {
res.data.forEach((item: FileStat) => {
const relativePath = path.relative(baseDir, item.filename)
const relative = webPath && urlPrefix + `/${path.join(webPath, relativePath)}`.replace(/\\/g, '/').replace(/\/+/g, '/')
if (item.type === 'directory') {
result.fullList.push(this.formatFolder(item, urlPrefix))
result.fullList.push(this.formatFolder(item, webPath ? relative : urlPrefix, !!webPath))
} 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 () {
const name = this.currentPicBedConfig.picBedName
switch (name) {
case 'tcyun':
return new API.TcyunApi(this.currentPicBedConfig.secretId, this.currentPicBedConfig.secretKey, this.logger)
case 'aliyun':
return new API.AliyunApi(this.currentPicBedConfig.accessKeyId, this.currentPicBedConfig.accessKeySecret, this.logger)
case 'qiniu':
return new API.QiniuApi(this.currentPicBedConfig.accessKey, this.currentPicBedConfig.secretKey, this.logger)
case 'upyun':
return new API.UpyunApi(this.currentPicBedConfig.bucketName, this.currentPicBedConfig.operator, this.currentPicBedConfig.password, this.logger)
case 'smms':
return new API.SmmsApi(this.currentPicBedConfig.token, this.logger)
case 'github':
return new API.GithubApi(this.currentPicBedConfig.token, this.currentPicBedConfig.githubUsername, this.currentPicBedConfig.proxy, this.logger)
case 'imgur':
return new API.ImgurApi(this.currentPicBedConfig.imgurUserName, this.currentPicBedConfig.accessToken, this.currentPicBedConfig.proxy, this.logger)
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':
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':
return new API.WebdavplistApi(this.currentPicBedConfig.endpoint, this.currentPicBedConfig.username, this.currentPicBedConfig.password, this.currentPicBedConfig.sslEnabled, this.currentPicBedConfig.proxy, this.logger)
default:
@@ -104,9 +108,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
getConfig<T> (name?: string): T {
if (!name) {
return this._config as unknown as T
} else {
return get(this._config, name)
}
return get(this._config, name)
}
saveConfig (config: IStringKeyMap): void {
@@ -149,6 +152,7 @@ export class ManageApi extends EventEmitter implements ManageApiType {
param?: IStringKeyMap | undefined
): Promise<any> {
let client
const name = this.currentPicBedConfig.picBedName.replace('plist', '')
switch (this.currentPicBedConfig.picBedName) {
case 'tcyun':
case 'aliyun':
@@ -170,15 +174,12 @@ export class ManageApi extends EventEmitter implements ManageApiType {
CreationDate: new Date().toISOString()
}]
case 'smms':
return [{
Name: 'smms',
Location: 'smms',
CreationDate: new Date().toISOString()
}]
case 'webdavplist':
case 'local':
case 'sftp':
return [{
Name: 'webdav',
Location: 'webdav',
Name: name,
Location: name,
CreationDate: new Date().toISOString()
}]
default:
@@ -314,6 +315,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'imgur':
case 's3plist':
case 'webdavplist':
case 'local':
case 'sftp':
try {
client = this.createClient() as any
return await client.getBucketListRecursively(param!)
@@ -357,6 +360,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'imgur':
case 's3plist':
case 'webdavplist':
case 'local':
case 'sftp':
try {
client = this.createClient() as any
return await client.getBucketListBackstage(param!)
@@ -427,6 +432,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'imgur':
case 's3plist':
case 'webdavplist':
case 'local':
case 'sftp':
try {
client = this.createClient() as any
const res = await client.deleteBucketFile(param!)
@@ -452,6 +459,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'github':
case 's3plist':
case 'webdavplist':
case 'local':
case 'sftp':
try {
client = this.createClient() as any
return await client.deleteBucketFolder(param!)
@@ -475,6 +484,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'upyun':
case 's3plist':
case 'webdavplist':
case 'local':
case 'sftp':
try {
client = this.createClient() as any
return await client.renameBucketFile(param!)
@@ -501,6 +512,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'imgur':
case 's3plist':
case 'webdavplist':
case 'local':
case 'sftp':
try {
client = this.createClient() as any
const res = await client.downloadBucketFile(param!)
@@ -533,6 +546,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'github':
case 's3plist':
case 'webdavplist':
case 'local':
case 'sftp':
try {
client = this.createClient() as any
return await client.createBucketFolder(param!)
@@ -559,6 +574,8 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'imgur':
case 's3plist':
case 'webdavplist':
case 'local':
case 'sftp':
try {
client = this.createClient() as any
return await client.uploadBucketFile(param!)
+66 -82
View File
@@ -4,7 +4,7 @@ import mime from 'mime-types'
import axios from 'axios'
import { app } from 'electron'
import crypto from 'crypto'
import got, { RequestError } from 'got'
import got, { OptionsOfTextResponseBody, RequestError } from 'got'
import { Stream } from 'stream'
import { promisify } from 'util'
import UpDownTaskQueue,
@@ -40,20 +40,13 @@ export const getFSFile = async (
}
}
export const isInputConfigValid = (config: any): boolean => {
if (
typeof config === 'object' &&
export function isInputConfigValid (config: any): boolean {
return typeof config === 'object' &&
!Array.isArray(config) &&
Object.keys(config).length > 0
) {
return true
}
return false
}
export const getFileMimeType = (filePath: string): string => {
return mime.lookup(filePath) || 'application/octet-stream'
}
export const getFileMimeType = (filePath: string): string => mime.lookup(filePath) || 'application/octet-stream'
const checkTempFolderExist = async () => {
const tempPath = path.join(app.getPath('downloads'), 'piclistTemp')
@@ -131,7 +124,7 @@ export const NewDownloader = async (
})
return true
} catch (e: any) {
logger && logger.error(formatError(e, { method: 'NewDownloader' }))
logger?.error(formatError(e, { method: 'NewDownloader' }))
fs.remove(savedFilePath)
instance.updateDownloadTask({
id,
@@ -179,13 +172,13 @@ export const gotUpload = async (
.then((res: any) => {
instance.updateUploadTask({
id,
progress: res && (res.statusCode === 200 || res.statusCode === 201) ? 100 : 0,
status: res && (res.statusCode === 200 || res.statusCode === 201) ? uploadTaskSpecialStatus.uploaded : commonTaskStatus.failed,
progress: res?.statusCode === 200 || res?.statusCode === 201 ? 100 : 0,
status: res?.statusCode === 200 || res?.statusCode === 201 ? uploadTaskSpecialStatus.uploaded : commonTaskStatus.failed,
finishTime: new Date().toLocaleString()
})
})
.catch((err: any) => {
logger && logger.error(formatError(err, { method: 'gotUpload' }))
logger?.error(formatError(err, { method: 'gotUpload' }))
instance.updateUploadTask({
id,
progress: 0,
@@ -213,42 +206,46 @@ export const formatError = (err: any, params:IStringKeyMap) => {
message: err.message ?? '',
stack: err.stack ?? ''
}
} else {
if (typeof err === 'object') {
return JSON.stringify(err) + JSON.stringify(params)
} else {
return String(err) + JSON.stringify(params)
}
}
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 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 commonResult = {
https: undefined,
http: undefined
}
if (!formatProxy) return commonResult
commonOptions.proxy = formatProxy.replace('127.0.0.1', 'localhost')
if (https) {
return formatProxy
? {
https: new HttpsProxyAgent({
keepAlive: true,
keepAliveMsecs: 1000,
rejectUnauthorized: false,
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined,
proxy: formatProxy.replace('127.0.0.1', 'localhost')
})
}
: {}
} else {
return formatProxy
? {
http: new HttpProxyAgent({
keepAlive: true,
keepAliveMsecs: 1000,
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined,
proxy: formatProxy.replace('127.0.0.1', 'localhost')
})
}
: {}
return {
https: new HttpsProxyAgent({
...commonOptions,
rejectUnauthorized: false
}),
http: undefined
}
}
return {
http: new HttpProxyAgent({
...commonOptions
}),
https: undefined
}
}
@@ -258,10 +255,8 @@ export const getInnerAgent = (proxy: any, sslEnabled: boolean = true) => {
return formatProxy
? {
agent: new https.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
...commonOptions,
rejectUnauthorized: false,
scheduling: 'lifo' as 'lifo' | 'fifo' | undefined,
host: formatProxy.host,
port: formatProxy.port
})
@@ -272,25 +267,20 @@ export const getInnerAgent = (proxy: any, sslEnabled: boolean = 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 (
@@ -301,23 +291,17 @@ export function getOptions (
body?: any,
timeout?: number,
proxy?: any
) {
const options = {
method: method?.toUpperCase(),
headers,
searchParams,
agent: getAgent(proxy),
timeout: {
request: timeout || 30000
},
body,
throwHttpErrors: false,
responseType
} as IStringKeyMap
Object.keys(options).forEach(key => {
options[key] === undefined && delete options[key]
})
return options
): OptionsOfTextResponseBody {
return {
...(method && { method: method.toUpperCase() }),
...(headers && { headers }),
...(searchParams && { searchParams }),
...(body && { body }),
...(responseType && { responseType }),
...(timeout !== undefined ? { timeout: { request: timeout } } : { timeout: { request: 30000 } }),
...(proxy && { agent: Object.fromEntries(Object.entries(getAgent(proxy)).filter(([, v]) => v !== undefined)) }),
throwHttpErrors: false
}
}
export const formatEndpoint = (endpoint: string, sslEnabled: boolean): string =>
+29 -29
View File
@@ -1,32 +1,33 @@
const AliyunAreaCodeName : IStringKeyMap = {
'oss-cn-hangzhou': '华东1杭州',
'oss-cn-shanghai': '华东2上海',
'oss-cn-nanjing': '华东5南京本地地域)',
'oss-cn-fuzhou': '华东6福州本地地域)',
'oss-cn-qingdao': '华北1青岛',
'oss-cn-beijing': '华北2北京',
'oss-cn-zhangjiakou': '华北3张家口',
'oss-cn-huhehaote': '华北5呼和浩特',
'oss-cn-wulanchabu': '华北6乌兰察布',
'oss-cn-shenzhen': '华南1深圳',
'oss-cn-heyuan': '华南2河源',
'oss-cn-guangzhou': '华南3广州',
'oss-cn-chengdu': '西南1成都',
'oss-cn-hongkong': '中国香港',
'oss-us-west-1': '美国硅谷',
'oss-us-east-1': '美国弗吉尼亚',
'oss-ap-northeast-1': '日本东京',
'oss-ap-northeast-2': '韩国首尔',
'oss-cn-hangzhou': '华东1(杭州)',
'oss-cn-shanghai': '华东2(上海)',
'oss-cn-nanjing': '华东5(南京)',
'oss-cn-fuzhou': '华东6(福州)',
'oss-cn-qingdao': '华北1(青岛)',
'oss-cn-beijing': '华北2(北京)',
'oss-cn-zhangjiakou': '华北3(张家口)',
'oss-cn-huhehaote': '华北5(呼和浩特)',
'oss-cn-wulanchabu': '华北6(乌兰察布)',
'oss-cn-shenzhen': '华南1(深圳)',
'oss-cn-heyuan': '华南2(河源)',
'oss-cn-guangzhou': '华南3(广州)',
'oss-cn-chengdu': '西南1(成都)',
'oss-cn-hongkong': '中国香港',
'oss-us-west-1': '美国(硅谷)',
'oss-us-east-1': '美国(弗吉尼亚)',
'oss-ap-northeast-1': '日本(东京)',
'oss-ap-northeast-2': '韩国(首尔)',
'oss-ap-southeast-1': '新加坡',
'oss-ap-southeast-2': '澳大利亚悉尼',
'oss-ap-southeast-3': '马来西亚吉隆坡',
'oss-ap-southeast-5': '印度尼西亚雅加达',
'oss-ap-southeast-6': '菲律宾马尼拉',
'oss-ap-southeast-7': '泰国曼谷',
'oss-ap-south-1': '印度孟买',
'oss-eu-central-1': '德国法兰克福',
'oss-eu-west-1': '英国伦敦',
'oss-me-east-1': '阿联酋迪拜'
'oss-ap-southeast-2': '澳大利亚(悉尼)',
'oss-ap-southeast-3': '马来西亚(吉隆坡)',
'oss-ap-southeast-5': '印度尼西亚(雅加达)',
'oss-ap-southeast-6': '菲律宾(马尼拉)',
'oss-ap-southeast-7': '泰国(曼谷)',
'oss-ap-south-1': '印度(孟买)',
'oss-eu-central-1': '德国(法兰克福)',
'oss-eu-west-1': '英国(伦敦)',
'oss-me-east-1': '阿联酋(迪拜)',
'oss-rg-china-mainland': '无地域属性'
}
const QiniuAreaCodeName : IStringKeyMap = {
@@ -61,8 +62,7 @@ const TencentAreaCodeName : IStringKeyMap = {
'na-ashburn': '弗吉尼亚(美东)',
'na-toronto': '多伦多',
'sa-saopaulo': '圣保罗',
'eu-frankfurt': '法兰克福',
'eu-moscow': '莫斯科'
'eu-frankfurt': '法兰克福'
}
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 {
try {
if (this.checkLogLevel(type, this.logLevel)) {
let log = `${dayjs().format(
'YYYY-MM-DD HH:mm:ss'
)} [PicList ${type.toUpperCase()}] `
let log = `${dayjs().format('YYYY-MM-DD HH:mm:ss')} [PicList ${type.toUpperCase()}] `
msg.forEach((item: ILogArgvTypeWithError) => {
if (item instanceof Error && type === 'error') {
log += `\n------Error Stack Begin------\n${util.format(
item?.stack
)}\n-------Error Stack End------- `
} else {
if (typeof item === 'object') {
if (item?.stack) {
log = log + `\n------Error Stack Begin------\n${util.format(
item.stack
)}\n-------Error Stack End------- `
}
item = JSON.stringify(item, (key, value) => {
if (key === 'stack') {
return undefined
}
return value
}, 2)
}
log += `${item as string} `
}
log += this.formatLogItem(item, type)
})
log += '\n'
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 (
type: string,
level: undefined | string | string[]
@@ -134,9 +129,8 @@ export class ManageLogger implements ILogger {
}
if (Array.isArray(level)) {
return level.some((item: string) => item === type || item === 'all')
} else {
return type === level
}
return type === level
}
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 {
private httpServer: http.Server
private config: IServerConfig
constructor () {
let config = picgo.getConfig<IServerConfig>('settings.server')
const result = this.checkIfConfigIsValid(config)
@@ -31,11 +32,7 @@ class Server {
}
private checkIfConfigIsValid (config: IObj | undefined) {
if (config && config.port && config.host && (config.enable !== undefined)) {
return true
} else {
return false
}
return config && config.port && config.host && (config.enable !== undefined)
}
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 path from 'path'
import { dbPathDir } from 'apis/core/datastore/dbChecker'
const STORE_PATH = dbPathDir()
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 deleteErrorMessage = `delete error. see ${LOG_PATH} for more detail.`
+5 -8
View File
@@ -21,14 +21,11 @@ function beforeOpen () {
*/
function resolveMacWorkFlow () {
const dest = `${os.homedir()}/Library/Services/Upload pictures with PicList.workflow`
if (fs.existsSync(dest)) {
return true
} else {
try {
fs.copySync(path.join(__static, 'Upload pictures with PicList.workflow'), dest)
} catch (e) {
console.log(e)
}
if (fs.existsSync(dest)) return true
try {
fs.copySync(path.join(__static, 'Upload pictures with PicList.workflow'), dest)
} 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>
<script lang="ts" setup>
//
import { useStore } from '@/hooks/useStore'
// Vue
import { onBeforeMount, onMounted, onUnmounted } from 'vue'
//
import { getConfig } from './utils/dataSender'
//
import type { IConfig } from 'piclist'
//
import bus from './utils/bus'
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 {
return path && path !== '/'
? `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}`
? `${path.replace(/^\/+|\/+$/, '')}/${fileName}`
: fileName
}
@@ -33,7 +33,7 @@ export default class AliyunApi {
try {
const client = AliyunApi.createClient(config)
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
} catch (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 TcyunApi from './tcyun'
import AliyunApi from './aliyun'
import QiniuApi from './qiniu'
import ImgurApi from './imgur'
import GithubApi from './github'
import UpyunApi from './upyun'
import AwsS3Api from './awss3'
import WebdavApi from './webdav'
import LocalApi from './local'
import DogeCloudApi from './dogecloud'
import HuaweicloudApi from './huaweiyun'
import AlistApi from './alist'
const apiMap: IStringKeyMap = {
aliyun: AliyunApi,
'aws-s3': AwsS3Api,
github: GithubApi,
imgur: ImgurApi,
local: LocalApi,
qiniu: QiniuApi,
sftpplist: SftpPlistApi,
smms: SmmsApi,
tcyun: TcyunApi,
aliyun: AliyunApi,
qiniu: QiniuApi,
imgur: ImgurApi,
github: GithubApi,
upyun: UpyunApi,
'aws-s3': AwsS3Api,
webdavplist: WebdavApi,
local: LocalApi
dogecloud: DogeCloudApi,
'huaweicloud-uploader': HuaweicloudApi,
alist: AlistApi
}
export default class ALLApi {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
const api = apiMap[configMap.type]
if (api) {
return await api.delete(configMap)
}
return false
return api ? await api.delete(configMap) : 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 {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
const { imgUrl, config: { accessKeyID, secretAccessKey, bucketName, region, endpoint, pathStyleAccess, bucketEndpoint, rejectUnauthorized } } = configMap
try {
const url = new URL(!/^https?:\/\//.test(imgUrl) ? `http://${imgUrl}` : imgUrl)
const fileKey = url.pathname
let endpointUrl
if (endpoint) {
if (!/^https?:\/\//.test(endpoint)) {
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
return ipcRenderer
? await ipcRenderer.invoke('delete-aws-s3-file',
getRawData(configMap)
)
: await removeFileFromS3InMain(getRawData(configMap))
} catch (error) {
console.log(error)
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 {
return path && path !== '/'
? `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}`
? `${path.replace(/^\/+|\/+$/, '')}/${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 {
static baseUrl: 'https://api.imgur.com/3'
private static async makeRequest (
method: 'delete',
url: string,
@@ -35,25 +37,23 @@ export default class ImgurApi {
}
static async delete (configMap: IConfigMap): Promise<boolean> {
const { config = {}, hash = '' } = configMap || {}
const { clientId = '', username = '', accessToken = '' } = config
const baseUrl = 'https://api.imgur.com/3'
let Authorization: string
let apiUrl: string
const {
config: { clientId = '', username = '', accessToken = '' } = {},
hash = ''
} = configMap
let Authorization: string, apiUrl: string
if (username && accessToken) {
Authorization = `Bearer ${accessToken}`
apiUrl = `${baseUrl}/account/${username}/image/${hash}`
apiUrl = `${ImgurApi.baseUrl}/account/${username}/image/${hash}`
} else if (clientId) {
Authorization = `Client-ID ${clientId}`
apiUrl = `${baseUrl}/image/${hash}`
apiUrl = `${ImgurApi.baseUrl}/image/${hash}`
} else {
return false
}
const headers = {
Authorization
}
const requestConfig: IConfig = {
headers,
headers: { Authorization },
timeout: 30000
}
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> {
const { hash } = configMap
if (!hash) {
console.error('SmmsApi.delete: invalid params')
console.error('Local.delete: invalid params')
return false
}
+2 -2
View File
@@ -17,7 +17,7 @@ export default class QiniuApi {
const qiniuConfig = new Qiniu.conf.Config()
try {
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 res = await new Promise((resolve, reject) => {
bucketManager.delete(bucket, key, (err, respBody, respInfo) => {
@@ -31,7 +31,7 @@ export default class QiniuApi {
}
})
}) as any
return res && res.respInfo.statusCode === 200
return res?.respInfo?.statusCode === 200
} catch (error) {
console.error(error)
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) {
key = `/${fileName}`
} else {
key = `/${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}`
key = `/${path.replace(/^\/+|\/+$/, '')}/${fileName}`
}
const result = await cos.deleteObject({
Bucket: bucket,
+1 -1
View File
@@ -11,7 +11,7 @@ export default class UpyunApi {
if (path === '/' || !path) {
key = fileName
} else {
key = `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}`
key = `${path.replace(/^\/+|\/+$/, '')}/${fileName}`
}
return await client.deleteFile(key)
} catch (error) {
+1 -1
View File
@@ -16,7 +16,7 @@ export default class WebdavApi {
if (path === '/' || !path) {
key = fileName
} else {
key = `${path.replace(/^\//, '').replace(/\/$/, '')}/${fileName}`
key = `${path.replace(/^\/+|\/+$/, '')}/${fileName}`
}
try {
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>
<el-image
:src="isShowThumbnail && item.isImage ?
base64Url
: require(`../manage/pages/assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
:src="imageSource"
fit="contain"
style="height: 100px;width: 100%;margin: 0 auto;"
>
@@ -13,7 +11,7 @@
</template>
<template #error>
<el-image
:src="require(`../manage/pages/assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
:src="iconPath"
fit="contain"
style="height: 100px;width: 100%;margin: 0 auto;"
/>
@@ -22,11 +20,13 @@
</template>
<script lang="ts" setup>
import { ref, onBeforeMount } from 'vue'
import { ref, onMounted, watch, computed } from 'vue'
import { getFileIconPath } from '@/manage/utils/common'
import { Loading } from '@element-plus/icons-vue'
const base64Url = ref('')
const success = ref(false)
const props = defineProps(
{
isShowThumbnail: {
@@ -48,18 +48,32 @@ const props = defineProps(
}
)
const urlCreateObjectURL = async () => {
await fetch(props.url, {
method: 'GET',
headers: props.headers
}).then(res => res.blob()).then(blob => {
base64Url.value = URL.createObjectURL(blob)
}).catch(err => {
const imageSource = computed(() => {
return (props.isShowThumbnail && props.item.isImage && success.value)
? base64Url.value
: require(`../manage/pages/assets/icons/${getFileIconPath(props.item.fileName ?? '')}`)
})
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)
})
}
}
onBeforeMount(async () => {
await urlCreateObjectURL()
})
watch(() => [props.url, props.headers], fetchImage, { deep: true })
onMounted(fetchImage)
</script>
@@ -34,6 +34,8 @@ import {
} from '~/universal/events/constants'
import $bus from '@/utils/bus'
import { sendToMain } from '@/utils/dataSender'
import { T as $T } from '@/i18n/index'
const inputBoxValue = ref('')
const showInputBoxVisible = ref(false)
const inputBoxOptions = reactive({
@@ -10,6 +10,7 @@
</template>
<script lang="ts" setup>
import { IToolboxItemCheckStatus } from '~/universal/types/enum'
interface IProps {
status: IToolboxItemCheckStatus
value: any
@@ -18,6 +18,7 @@
import { CircleCloseFilled, Loading, SuccessFilled } from '@element-plus/icons-vue'
import { computed } from 'vue'
import { IToolboxItemCheckStatus } from '~/universal/types/enum'
interface IProps {
status: IToolboxItemCheckStatus
}
+49 -4
View File
@@ -47,6 +47,15 @@
</el-icon>
</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
style="padding-top: 22px;"
class="main-content"
@@ -105,7 +114,7 @@
</el-sub-menu>
<el-menu-item :index="routerConfig.SETTING_PAGE">
<el-icon>
<Setting />
<Tools />
</el-icon>
<span>{{ $T('PICLIST_SETTINGS') }}</span>
</el-menu-item>
@@ -233,9 +242,9 @@
</div>
</template>
<script lang="ts" setup>
// import { Component, Vue, Watch } from 'vue-property-decorator'
// Element Plus
import {
Setting,
Tools,
UploadFilled,
PictureFilled,
Menu,
@@ -248,20 +257,42 @@ import {
Link,
ArrowUpBold
} from '@element-plus/icons-vue'
// Element Plus
import { ElMessage as $message, ElMessageBox } from 'element-plus'
//
import { T as $T } from '@/i18n/index'
// Vue
import { ref, onBeforeUnmount, Ref, onBeforeMount, watch, nextTick, reactive } from 'vue'
// Vue Router
import { onBeforeRouteUpdate, useRouter } from 'vue-router'
//
import QrcodeVue from 'qrcode.vue'
// Lodash pick
import pick from 'lodash/pick'
// package.json
import pkg from 'root/package.json'
//
import * as config from '@/router/config'
// Electron
import {
ipcRenderer,
IpcRendererEvent,
clipboard
} from 'electron'
//
import InputBoxDialog from '@/components/InputBoxDialog.vue'
//
import {
MINIMIZE_WINDOW,
CLOSE_WINDOW,
@@ -271,8 +302,13 @@ import {
GET_PICBEDS,
OPEN_URL
} from '~/universal/events/constants'
//
import { getConfig, sendToMain } from '@/utils/dataSender'
// Piclist
import { IConfig } from 'piclist'
const version = ref(process.env.NODE_ENV === 'production' ? pkg.version : 'Dev')
const routerConfig = reactive(config)
const defaultActive = ref(routerConfig.UPLOAD_PAGE)
@@ -284,9 +320,11 @@ const qrcodeVisible = ref(false)
const picBedConfigString = ref('')
const choosedPicBedForQRCode: Ref<string[]> = ref([])
const isAlwaysOnTop = ref(false)
const keepAlivePages = $router.getRoutes().filter(item => item.meta.keepAlive).map(item => item.name as string)
const progressShow = ref(false)
const progressPercentage = ref(0)
onBeforeMount(() => {
os.value = process.platform
sendToMain(GET_PICBEDS)
@@ -298,6 +336,10 @@ onBeforeMount(() => {
ipcRenderer.on(SHOW_MAIN_PAGE_DONATION, () => {
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) => {
@@ -393,6 +435,9 @@ onBeforeRouteUpdate(async (to) => {
onBeforeUnmount(() => {
ipcRenderer.removeListener(GET_PICBEDS, getPicBeds)
ipcRenderer.removeAllListeners(SHOW_MAIN_PAGE_QRCODE)
ipcRenderer.removeAllListeners(SHOW_MAIN_PAGE_DONATION)
ipcRenderer.removeAllListeners('updateProgress')
})
</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;"
>
</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>
</template>
</el-popover>
@@ -251,103 +258,123 @@
</template>
<script lang="ts" setup>
// Vue
import { reactive, ref, onMounted, computed } from 'vue'
//
import { supportedPicBedList } from '../utils/constants'
// Element Plus
import { Delete, Edit, Pointer, InfoFilled } from '@element-plus/icons-vue'
// Element Plus
import { ElMessage, ElNotification } from 'element-plus'
//
import { getConfig, saveConfig, removeConfig } from '../utils/dataSender'
// Electron
import { shell } from 'electron'
// Vue Router
import { useRouter } from 'vue-router'
//
import { useManageStore } from '../store/manageStore'
//
import { formObjToTableData, svg } from '../utils/common'
//
import { getConfig as getPicBedsConfig } from '@/utils/dataSender'
//
import { formatEndpoint } from '~/main/manage/utils/common'
import { isNeedToShorten, safeSliceF } from '#/utils/common'
//
import { T as $T } from '@/i18n'
const manageStore = useManageStore()
const router = useRouter()
const isLoading = ref(false)
const activeName = ref('login')
const configResult:IStringKeyMap = reactive({})
const existingConfiguration = reactive({} as IStringKeyMap)
const dataForTable = reactive([] as any[])
const allConfigAliasMap = reactive({} as IStringKeyMap)
const router = useRouter()
const manageStore = useManageStore()
const isLoading = ref(false)
const currentAliasList = reactive([] as string[])
const rules = ruleMap(supportedPicBedList)
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 sorted
})
const currentAliasList = reactive([] as string[])
const importedNewConfig: IStringKeyMap = {}
const ruleMap = (options: IStringKeyMap) => {
function ruleMap (options: IStringKeyMap) {
const rule:any = {}
Object.keys(options).forEach((key) => {
const item = options[key].options
item.forEach((option: string) => {
const keyName = key + '.' + option
if (options[key].configOptions[option].rule) {
rule[keyName] = options[key].configOptions[option].rule
const configOptions = options[key].configOptions[option]
const keyName = `${key}.${option}`
if (configOptions.rule) {
rule[keyName] = configOptions.rule
}
if (options[key].configOptions[option].default) {
configResult[keyName] = options[key].configOptions[option].default
if (configOptions.default) {
configResult[keyName] = configOptions.default
}
})
})
return rule
}
const rules = ruleMap(supportedPicBedList)
const getDataForTable = () => {
function getDataForTable () {
for (const key in existingConfiguration) {
const obj = {} as IStringKeyMap
for (const option in existingConfiguration[key]) {
obj[option] = existingConfiguration[key][option]
}
dataForTable.push(obj)
dataForTable.push({ ...existingConfiguration[key] as IStringKeyMap })
}
}
const getExistingConfig = async (name:string) => {
async function getExistingConfig (name:string) {
if (name === 'login') {
getAllConfigAliasArray()
return
}
currentAliasList.length = 0
const result = await getConfig<any>('picBed')
if (!result) {
existingConfiguration[name] = { fail: '暂无配置' }
}
for (const key in existingConfiguration) {
delete existingConfiguration[key]
}
for (const key in result) {
if (result[key].picBedName === name) {
existingConfiguration[key] = result[key]
currentAliasList.push(key)
if (!result || typeof result !== 'object' || Object.keys(result).length === 0) {
existingConfiguration[name] = { fail: '暂无配置' }
} else {
for (const key in result) {
if (result[key].picBedName === name) {
existingConfiguration[key] = result[key]
currentAliasList.push(key)
}
}
}
dataForTable.length = 0
getDataForTable()
handleConfigImport(currentAliasList[0])
}
const getAliasList = () => {
const aliasList = [] as string[]
for (const key in existingConfiguration) {
aliasList.push(existingConfiguration[key].alias)
}
return aliasList
function getAliasList () {
return Object.values(existingConfiguration).map(item => item.alias)
}
const handleConfigChange = async (name: string) => {
async function handleConfigChange (name: string) {
const aliasList = getAliasList()
const allKeys = Object.keys(supportedPicBedList[name].configOptions)
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) {
const resultKey = name + '.' + key
if (supportedPicBedList[name].configOptions[key].required) {
@@ -442,24 +469,31 @@ const handleConfigChange = async (name: string) => {
}
const handleConfigReset = (name: string) => {
let keys = Object.keys(configResult)
keys = keys.filter((key) => key.startsWith(name))
const keys = Object.keys(configResult).filter((key) => key.startsWith(name))
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 commonNoticeConfig = {
title: $T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_NAME'),
duration: 2000,
customClass: 'notification',
offset: 100
}
try {
removeConfig('picBed', name)
ElNotification(
{
title: $T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_NAME'),
...commonNoticeConfig,
message: `${$T('MANAGE_LOGIN_PAGE_PANE_CONFIG_CHANGE_NOTICE_MESSAGE_C')}${name}`,
type: 'success',
duration: 2000,
customClass: 'notification',
offset: 100,
position: 'bottom-right'
}
)
@@ -468,12 +502,9 @@ const handleConfigRemove = (name: string) => {
} catch (error) {
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')}`,
type: 'error',
duration: 2000,
customClass: 'notification',
offset: 100,
position: 'bottom-right'
}
)
@@ -485,17 +516,13 @@ const getAllConfigAliasArray = async () => {
for (const key in allConfigAliasMap) {
delete allConfigAliasMap[key]
}
if (!result) {
return
}
let i = 0
Object.keys(result).forEach((key) => {
allConfigAliasMap[i] = {
alias: result[key].alias,
picBedName: result[key].picBedName,
config: result[key]
if (!result) return
Object.entries(result).forEach(([, value]: [string, any], index) => {
allConfigAliasMap[index] = {
alias: value.alias,
picBedName: value.picBedName,
config: value
}
i++
})
}
@@ -524,244 +551,271 @@ const handleConfigClick = async (item: any) => {
function handleConfigImport (alias: string) {
const selectedConfig = existingConfiguration[alias]
if (selectedConfig) {
supportedPicBedList[selectedConfig.picBedName].options.forEach((option: any) => {
if (selectedConfig[option] !== undefined) {
configResult[selectedConfig.picBedName + '.' + option] = selectedConfig[option]
}
if (typeof selectedConfig[option] === 'boolean') {
configResult[selectedConfig.picBedName + '.' + option] = selectedConfig[option]
}
})
}
if (!selectedConfig) return
supportedPicBedList[selectedConfig.picBedName].options.forEach((option: any) => {
if (selectedConfig[option] !== undefined) {
configResult[selectedConfig.picBedName + '.' + option] = selectedConfig[option]
}
})
}
async function getCurrentConfigList () {
const configList = await getPicBedsConfig<any>('uploader') ?? {}
const pbList = ['aliyun', 'tcyun', 'upyun', 'qiniu', 'smms', 'qiniu', 'github', 'webdavplist', 'aws-s3', 'imgur']
const filteredConfigList = pbList.map((pb) => {
const pbList = ['aliyun', 'aws-s3', 'github', 'imgur', 'local', 'qiniu', 'sftpplist', 'smms', 'tcyun', 'upyun', 'webdavplist']
const filteredConfigList = pbList.flatMap((pb) => {
const config = configList[pb]
if (config && config.configList.length > 0) {
config.configList.forEach((item: any) => {
item.type = pb
})
return config
} else {
return null
}
}).filter((config) => config && config.configList.length > 0)
return config?.configList?.length ? config.configList.map((item: any) => ({ ...item, type: pb })) : []
})
await getAllConfigAliasArray()
const promises: Promise<any>[] = []
for (const config of filteredConfigList.flatMap((config) => config.configList)) {
const pb = config.type
promises.push(transUpToManage(config, pb))
const autoImport = await getPicBedsConfig<boolean>('settings.autoImport') || false
if (!autoImport) return
const autoImportPicBed = initArray(await getPicBedsConfig<string | string[]>('settings.autoImportPicBed') || '', [])
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) {
for (const key in allConfigAliasMap) {
if (allConfigAliasMap[key].alias === alias) {
return true
}
}
return false
return Object.values(allConfigAliasMap).some((item) => item.alias === alias)
}
async function transUpToManage (config: IUploaderConfigListItem, picBedName: string) {
const autoImport = await getConfig<boolean>('settings.autoImport') || false
if (!autoImport) {
return
function initArray (arrayT: string | string[], defaultValue: string[]) {
if (!Array.isArray(arrayT)) {
arrayT = arrayT ? [arrayT] : defaultValue
}
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 = {}
switch (picBedName) {
case 'smms':
alias = `smms-${config._configName ?? 'Default'}-imp`
if (!config.token || isImported(alias)) {
return
}
resultMap.alias = alias
resultMap.picBedName = 'smms'
resultMap.token = config.token
resultMap.paging = true
saveConfig(`picBed.${resultMap.alias}`, resultMap)
if (!config.token) return
Object.assign(resultMap, {
...commonConfig,
token: config.token
})
break
case 'aliyun':
if (!config.accessKeyId || !config.accessKeySecret) {
return
}
resultMap.alias = `aliyun-${config._configName ?? 'Default'}-imp`
resultMap.picBedName = 'aliyun'
resultMap.accessKeyId = config.accessKeyId
resultMap.accessKeySecret = config.accessKeySecret
resultMap.bucketName = ''
resultMap.baseDir = '/'
resultMap.paging = true
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
if (!config.accessKeyId || !config.accessKeySecret) return
Object.assign(resultMap, {
...commonConfig,
accessKeyId: config.accessKeyId,
accessKeySecret: config.accessKeySecret,
bucketName: '',
baseDir: '/',
itemsPerPage: 50,
isAutoCustomUrl: !config.customUrl,
transformedConfig: JSON.stringify(config.customUrl
? {
[config.bucketName]: {
customUrl: config.urlPrefix
[config.bucket]: {
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
case 'imgur':
alias = `imgur-${config._configName ?? 'Default'}-imp`
if (!config.username || !config.accessToken || isImported(alias)) {
return
}
resultMap.alias = alias
resultMap.picBedName = 'imgur'
resultMap.imgurUserName = config.username
resultMap.accessToken = config.accessToken
resultMap.proxy = config.proxy || ''
saveConfig(`picBed.${resultMap.alias}`, resultMap)
if (!config.username || !config.accessToken) return
Object.assign(resultMap, {
...commonConfig,
username: config.username,
accessToken: config.accessToken,
proxy: ''
})
delete resultMap.paging
break
default:
return
}
manageStore.refreshConfig()
importedNewConfig[alias] = resultMap
}
onMounted(async () => {
+132 -103
View File
@@ -127,7 +127,7 @@
<el-icon
class="layout__menu__setting__item__icon"
>
<Setting />
<Tools />
</el-icon>
{{ $T('MANAGE_MAIN_PAGE_SETTING') }}
</span>
@@ -273,31 +273,56 @@
</template>
<script lang="ts" setup>
// Vue
import { ref, reactive, computed, onBeforeMount, watch } from 'vue'
// Electron
import { shell } from 'electron'
//
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'
// Element Plus
import { ElNotification } from 'element-plus'
//
import { invokeToMain } from '../utils/dataSender'
//
import { newBucketConfig } from '../utils/newBucketConfig'
//
import { useManageStore } from '../store/manageStore'
//
import { T as $T } from '@/i18n'
import path from 'path'
const manageStore = useManageStore() as any
const route = useRoute()
const router = useRouter()
const currentAlias = ref(route.query.alias as string)
const currentPicBedName = ref(route.query.picBedName as string)
let allPicBedConfigure = JSON.parse(route.query.allPicBedConfigure as string)
let currentPagePicBedConfig = reactive(JSON.parse(route.query.config as string))
const picBedSwitchDialogVisible = ref(false)
const newBucketConfigResult: IStringKeyMap = reactive({})
const bucketList = ref({} as IStringKeyMap)
const currentSelectedBucket = ref('')
const isLoadingBucketList = ref(false)
const bucketNameList = ref([] as string[])
const isLoadingBucketList = ref(false)
const nweBucketDrawerVisible = ref(false)
const picBedSwitchDialogVisible = ref(false)
watch(route, async (newRoute) => {
if (newRoute.fullPath.split('?')[0] === '/main-page/manage-main-page') {
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 urlMap : IStringKeyMap = {
smms: 'https://smms.app',
aliyun: 'https://oss.console.aliyun.com',
github: 'https://github.com',
imgur: 'https://imgur.com',
aliyun: 'https://oss.console.aliyun.com',
local: 'https://piclist.cn',
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',
upyun: 'https://console.upyun.com',
s3plist: 'https://aws.amazon.com/cn/s3/',
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 rule: IStringKeyMap = {}
Object.keys(options).forEach((key) => {
const item = options[key].options
item.forEach((option: string) => {
const keyName = `${key}.${option}`
if (options[key].configOptions[option].rule) {
rule[keyName] = options[key].configOptions[option].rule
}
if (options[key].configOptions[option].default) {
newBucketConfigResult[keyName] = options[key].configOptions[option].default
}
})
})
return rule
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,
sftp: '',
smms: galleryT,
imgur: galleryT,
github: repositoryT,
webdavplist: '',
local: ''
}
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
}
const createNewBucket = (picBedName: string) => {
const allKeys = Object.keys(newBucketConfig[picBedName].configOptions)
const resultMap: IStringKeyMap = {}
for (const key of allKeys) {
function createNewBucket (picBedName: string) {
const configOptions = newBucketConfig[picBedName].configOptions
const resultMap: IStringKeyMap = Object.keys(configOptions).reduce((result, key) => {
const resultKey = `${picBedName}.${key}`
if (newBucketConfig[picBedName].configOptions[key].default !== undefined && newBucketConfigResult[resultKey] === '') {
resultMap[key] = newBucketConfig[picBedName].configOptions[key].default
} else if (newBucketConfigResult[resultKey] === undefined) {
if (newBucketConfig[picBedName].configOptions[key].default !== undefined) {
resultMap[key] = newBucketConfig[picBedName].configOptions[key].default
} else {
resultMap[key] = ''
}
} else {
resultMap[key] = newBucketConfigResult[resultKey]
}
}
const defaultValue = configOptions[key].default
const resultValue = newBucketConfigResult[resultKey]
result[key] = resultValue === '' && defaultValue !== undefined
? defaultValue
: resultValue === undefined ? defaultValue ?? '' : resultValue
return result
}, {} as IStringKeyMap)
if (currentPicBedName.value === 'tcyun') {
resultMap.BucketName = resultMap.BucketName + '-' + currentPagePicBedConfig.appId
resultMap.BucketName = `${resultMap.BucketName}-${currentPagePicBedConfig.appId}`
}
const res = invokeToMain('createBucket', currentAlias, resultMap)
res.then((result: any) => {
invokeToMain('createBucket', currentAlias, resultMap).then((result: any) => {
if (result) {
ElNotification({
title: $T('MANAGE_MAIN_PAGE_TIPS'),
@@ -389,12 +430,14 @@ const createNewBucket = (picBedName: string) => {
})
}
const getBucketList = async () => {
async function getBucketList () {
bucketList.value = {}
bucketNameList.value = []
isLoadingBucketList.value = true
const result = await invokeToMain('getBucketList', currentAlias.value)
isLoadingBucketList.value = false
if (result.length > 0) {
result.forEach((item: any) => {
bucketList.value[item.Name] = item
@@ -403,28 +446,34 @@ const getBucketList = async () => {
}
}
const handleSelectMenu = (bucketName: string) => {
const transformedConfig = JSON.parse(manageStore.config.picBed[currentAlias.value].transformedConfig ?? '{}')
let prefix = transformedConfig[bucketName]?.baseDir
if (prefix === '' || prefix === undefined) {
prefix = '/'
function transPathToUnix (filePath: string | undefined) {
if (!filePath) return ''
return process.platform === 'win32' ? filePath.split(path.sep).join(path.posix.sep).replace(/^\/+|\/+$/g, '') : filePath.replace(/^\/+|\/+$/g, '')
}
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 {
!prefix.startsWith('/') && (prefix = `/${prefix}`)
!prefix.endsWith('/') && (prefix = `${prefix}/`)
prefix = prefix.startsWith('/') ? 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 = {
prefix,
bucketName,
customUrl,
picBedName,
alias,
bucketConfig,
cdnUrl
customUrl: transformedConfig[bucketName]?.customUrl ?? '',
picBedName: cpicBedName,
alias: currentAlias.value,
bucketConfig: bucketList.value[bucketName],
cdnUrl: currentPicBedConfig.customUrl,
baseDir: prefix,
webPath: currentPicBedConfig.webPath || ''
}
currentSelectedBucket.value = bucketName
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) {
if (picBedAlias === 'main') {
router.push({
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 {
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 {
currentAlias.value = picBedAlias
currentPicBedName.value = allPicBedConfigure[picBedAlias].picBedName
currentPagePicBedConfig = allPicBedConfigure[picBedAlias]
picBedSwitchDialogVisible.value = false
currentSelectedBucket.value = ''
getBucketList()
}
currentAlias.value = picBedAlias
currentPicBedName.value = allPicBedConfigure[picBedAlias].picBedName
currentPagePicBedConfig = allPicBedConfigure[picBedAlias]
picBedSwitchDialogVisible.value = false
currentSelectedBucket.value = ''
getBucketList()
}
}
const changePicBed = () => {
function changePicBed () {
picBedSwitchDialogVisible.value = true
}
const openBucketPageSetting = () => {
function openBucketPageSetting () {
router.push({
path: '/main-page/manage-main-page/manage-setting-page'
})
+21 -6
View File
@@ -513,14 +513,29 @@
</template>
<script lang="ts" setup>
// Element Plus
import { InfoFilled, Folder } from '@element-plus/icons-vue'
// Vue
import { ref, reactive, onBeforeMount, watch, onBeforeUnmount } from 'vue'
//
import { getConfig, saveConfig, invokeToMain } from '../utils/dataSender'
// Element Plus
import { ElMessage } from 'element-plus'
//
import { useManageStore } from '../store/manageStore'
import { fileCacheDbInstance } from '../store/bucketFileDb'
//
import { formatFileSize, customRenameFormatTable } from '../utils/common'
//
import { T as $T } from '@/i18n'
//
import { selectDownloadFolder } from '../utils/static'
const manageStore = useManageStore()
@@ -597,21 +612,21 @@ async function initData () {
form.timestampRename = config.settings.timestampRename ?? false
form.randomStringRename = config.settings.randomStringRename ?? 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.isShowThumbnail = config.settings.isShowThumbnail ?? false
form.isShowList = config.settings.isShowList ?? false
form.isIgnoreCase = config.settings.isIgnoreCase ?? false
form.isForceCustomUrlHttps = config.settings.isForceCustomUrlHttps ?? true
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.isDownloadFileKeepDirStructure = config.settings.isDownloadKeepDirStructure ?? false
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 () {
+15 -12
View File
@@ -19,35 +19,38 @@ export interface IFileCache {
* new picbed will add a plist suffix to distinguish from picgo
*/
export class FileCacheDb extends Dexie {
tcyun: Table<IFileCache, string>
aliyun: Table<IFileCache, string>
qiniu: Table<IFileCache, string>
github: Table<IFileCache, string>
smms: Table<IFileCache, string>
upyun: 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>
sftp: Table<IFileCache, string>
upyun: Table<IFileCache, string>
webdavplist: Table<IFileCache, string>
localplist: Table<IFileCache, string>
constructor () {
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) => {
acc[cur] = '&key, value'
return acc
}, {} as IStringKeyMap)
this.version(3).stores(tableNamesMap)
this.tcyun = this.table('tcyun')
this.version(5).stores(tableNamesMap)
this.aliyun = this.table('aliyun')
this.qiniu = this.table('qiniu')
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.upyun = this.table('upyun')
this.imgur = this.table('imgur')
this.s3plist = this.table('s3plist')
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 path from 'path'
// 加密库
import crypto from 'crypto'
// 可用图标列表
import { availableIconList } from './icon'
// 数据发送工具函数
import { getConfig } from './dataSender'
import { handleUrlEncode } from '~/universal/utils/common'
// 工具函数
import { handleUrlEncode, safeSliceF, isNeedToShorten } from '~/universal/utils/common'
export function randomStringGenerator (length: number): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
@@ -41,15 +52,17 @@ export function renameFileNameWithCustomString (oldName: string, customFormat: s
}, customFormat) + ext
}
export function renameFile (typeMap : IStringKeyMap, oldName: string): string {
if (typeMap.timestampRename) {
return renameFileNameWithTimestamp(oldName)
} else if (typeMap.randomStringRename) {
return renameFileNameWithRandomString(oldName, 20)
} else if (typeMap.customRename) {
return renameFileNameWithCustomString(oldName, typeMap.customRenameFormat)
export function renameFile ({ timestampRename, randomStringRename, customRename, customRenameFormat }: IStringKeyMap, oldName = ''): string {
switch (true) {
case timestampRename:
return renameFileNameWithTimestamp(oldName)
case randomStringRename:
return renameFileNameWithRandomString(oldName, 20)
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> {
@@ -92,7 +105,7 @@ export function formatFileName (fileName: string, length: number = 20) {
let ext = path.extname(fileName)
ext = ext.length > 5 ? ext.slice(ext.length - 5) : 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)
@@ -123,7 +136,7 @@ export interface IHTTPProxy {
}
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)) {
const { protocol, hostname, port } = new URL(proxy)
return type === 'string'
@@ -133,16 +146,15 @@ export const formatHttpProxy = (proxy: string | undefined, type: 'object' | 'str
port: Number(port),
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 = `
+234 -9
View File
@@ -22,7 +22,7 @@ const itemsPerPageRule = [
trigger: 'change'
},
{
validator: (rule: any, value: any, callback: any) => {
validator: (_rule: any, value: any, callback: any) => {
if (value < 20 || value > 1000) {
callback(new Error($T('MANAGE_CONSTANT_ITEMS_PAGE_RULE_MESSAGE_C')))
} else {
@@ -40,8 +40,8 @@ const aliasRule = [
trigger: 'blur'
},
{
validator: (rule: any, value: any, callback: any) => {
const reg = /^[\u4e00-\u9fa5_a-zA-Z0-9-]+$/
validator: (_rule: any, value: any, callback: any) => {
const reg = /^[\u4e00-\u9fff_a-zA-Z0-9-]+$/
if (!reg.test(value)) {
callback(new Error($T('MANAGE_CONSTANT_ALIAS_RULE_MESSAGE_B')))
} else {
@@ -469,7 +469,7 @@ export const supportedPicBedList: IStringKeyMap = {
trigger: 'change'
},
{
validator: (rule: any, value: any, callback: any) => {
validator: (_rule: any, value: any, callback: any) => {
if (value) {
const customUrlList = value.split(',')
const customUrlValid = customUrlList.every((customUrl: string) => {
@@ -642,6 +642,13 @@ export const supportedPicBedList: IStringKeyMap = {
default: '/',
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: {
required: true,
description: $T('MANAGE_CONSTANT_S3_PAGING_DESC'),
@@ -659,7 +666,7 @@ export const supportedPicBedList: IStringKeyMap = {
}
},
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',
referenceText: $T('MANAGE_CONSTANT_S3_REFER_TEXT')
},
@@ -712,8 +719,7 @@ export const supportedPicBedList: IStringKeyMap = {
description: $T('MANAGE_CONSTANT_WEBDAV_BASE_DIR_DESC'),
placeholder: $T('MANAGE_CONSTANT_WEBDAV_BASE_DIR_PLACEHOLDER'),
type: 'string',
default: '/',
tooltip: baseDirTooltip
default: '/'
},
customUrl: {
required: false,
@@ -723,7 +729,7 @@ export const supportedPicBedList: IStringKeyMap = {
tooltip: $T('MANAGE_CONSTANT_WEBDAV_CUSTOM_URL_TOOLTIP'),
rule: [
{
validator: (rule: any, value: any, callback: any) => {
validator: (_rule: any, value: any, callback: any) => {
if (value) {
if (!/^https?:\/\/.+/.test(value)) {
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: {
required: false,
description: $T('MANAGE_CONSTANT_WEBDAV_PROXY_DESC'),
@@ -754,8 +768,219 @@ export const supportedPicBedList: IStringKeyMap = {
}
},
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',
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