Merge commit '9d63799d5a60b61eedfc1e2d35efe6cc93688a1d' into release
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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` 向用户展示该语言的名称。
|
||||
|
||||
@@ -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反馈。
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`:
|
||||
|
||||

|
||||
|
||||
@@ -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的支持。
|
||||
|
||||
## 应用截图
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 微信交流群
|
||||
|
||||
@@ -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 的应用图标。
|
||||
|
||||
### 生产模式
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: 完成时间
|
||||
|
||||
@@ -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: 完成時間
|
||||
|
||||
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 50 KiB |
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 : ''
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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!)
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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.`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 8.3 KiB |
@@ -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 () => {
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||