mirror of
https://github.com/Molunerfinn/PicGo.git
synced 2026-09-20 11:17:32 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9acceea42b | ||
|
|
dcc64b61d7 | ||
|
|
bad47fb145 | ||
|
|
e9d9ae66e9 | ||
|
|
9506aadef5 | ||
|
|
3980ef89fa |
@@ -333,10 +333,10 @@ jobs:
|
||||
- name: List dist
|
||||
run: ls -la dist/
|
||||
|
||||
- name: Upload to release.picgo.app
|
||||
- name: Upload update metadata to release.picgo.app
|
||||
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.test_upload_dist || github.event.inputs.test_upload_dist_to_dev
|
||||
run: |
|
||||
ARGS="--all"
|
||||
ARGS="--all --version-files-only"
|
||||
|
||||
if [[ "${{ github.event.inputs.test_upload_dist_to_dev }}" == "true" ]]; then
|
||||
ARGS="$ARGS --dev"
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
name: Mirror Release to AtomGit
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- atomgit-release-ci-test
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: Published GitHub release tag to mirror
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: mirror-atomgit-${{ github.event.release.tag_name || inputs.release_tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
mirror-release:
|
||||
name: Mirror GitHub Release to AtomGit
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag || 'v3.0.2' }}
|
||||
ATOM_OWNER: ${{ vars.ATOM_OWNER || 'Molunerfinn' }}
|
||||
ATOM_REPO: ${{ vars.ATOM_REPO || 'PicGo' }}
|
||||
ATOM_TARGET_COMMITISH: ${{ vars.ATOM_TARGET_COMMITISH }}
|
||||
ATOM_UPLOAD_TIMEOUT_MINUTES: ${{ vars.ATOM_UPLOAD_TIMEOUT_MINUTES || '70' }}
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Read GitHub release metadata
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release view "$RELEASE_TAG" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--json tagName,name,body,isDraft,isPrerelease,targetCommitish \
|
||||
> github-release.json
|
||||
|
||||
- name: Download GitHub release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
mkdir -p dist
|
||||
gh release download "$RELEASE_TAG" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--dir dist
|
||||
|
||||
- name: Publish AtomGit release
|
||||
env:
|
||||
ATOM_PAT: ${{ secrets.ATOM_PAT }}
|
||||
run: node scripts/publish-atomgit-release.js
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Notify Homepage
|
||||
|
||||
# Releases are created as drafts, so a tag push is too early -- the download
|
||||
# page reads releases/latest, which only reflects a release once it is
|
||||
# published. Firing on `published` also covers the case where a draft sits for
|
||||
# days before going live.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
rebuild-homepage:
|
||||
name: Rebuild picgo.app
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger homepage deploy
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HOMEPAGE_DISPATCH_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$GH_TOKEN" ]; then
|
||||
echo "::error::HOMEPAGE_DISPATCH_TOKEN is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The homepage builds its download list from the GitHub API at build
|
||||
# time, so it needs a rebuild to pick up a new release.
|
||||
gh workflow run deploy.yml \
|
||||
--repo PicGo/PicGo-Homepage \
|
||||
--ref production
|
||||
|
||||
echo "Requested a production rebuild of picgo.app"
|
||||
@@ -1,2 +1 @@
|
||||
pnpm check
|
||||
pnpm run lint:dpdm
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<div align="center" markdown="1">
|
||||
<sup>Special thanks to:</sup>
|
||||
<br>
|
||||
<a href="https://www.nocobase.com/?utm_source=picgo">
|
||||
<img alt="NocoBase sponsorship" width="400" src="https://static-docs.nocobase.com/Logo-Black.png">
|
||||
<a href="https://go.warp.dev/picgo">
|
||||
<img alt="Warp sponsorship" width="400" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-03.png">
|
||||
</a>
|
||||
|
||||
### [NocoBase, AI + No-Code Build reliable business systems](https://www.nocobase.com/?utm_source=picgo)
|
||||
### [Warp, the intelligent terminal for developers](https://go.warp.dev/picgo)
|
||||
[Available for macOS, Linux, & Windows](https://go.warp.dev/picgo)<br>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -91,20 +92,6 @@ PicGo is built around a fast, low-friction image upload experience:
|
||||
- **Even more possibilities**: image compression, watermarking, renaming, Markdown image migration, and more.
|
||||
- Explore plugins: [Awesome-PicGo](https://github.com/PicGo/Awesome-PicGo)
|
||||
|
||||
### 🤖 AI-friendly
|
||||
More and more writing is handed off to AI. But the screenshots and charts it produces sit on your disk as ``, and they need to become real links before you publish. PicGo lets AI upload them too:
|
||||
|
||||
- **[PicGo Skills](https://github.com/PicGo/skills)**: official Agent Skills that teach AI when and how to upload. Works with any AI tool that supports the skills format.
|
||||
```bash
|
||||
npx skills@latest add PicGo/skills
|
||||
```
|
||||
- **[DeepSeek Harness plugin](https://github.com/PicGo/dsh-plugin)**: install it in [dsh](https://github.com/deepseek-ai/deepseek-harness) and your agent can upload to your image host on its own.
|
||||
```bash
|
||||
dsh plugin --profile web add @picgo/dsh-plugin
|
||||
```
|
||||
|
||||
Both reuse the image hosts and plugins you already configured in PicGo—nothing to set up twice. Read more in [this post](https://picgo.app/blog/2026/picgo-deepseek-harness-plugin/).
|
||||
|
||||
### 🛠 Developer-friendly
|
||||
- **HTTP API**: upload via HTTP requests (v2.2.0+), making it easy to integrate with other tools.
|
||||
- **Open source**: fully open-source and transparent.
|
||||
@@ -176,8 +163,6 @@ Electron binaries are stored under `~/.electron/`. If you need to refresh them,
|
||||
- [vs-picgo](https://github.com/PicGo/vs-picgo): PicGo for VS Code.
|
||||
- [flutter-picgo](https://github.com/PicGo/flutter-picgo): mobile app (Android & iOS).
|
||||
- [PicHoro](https://github.com/Kuingsmile/PicHoro): another mobile app compatible with PicGo config (Android only for now).
|
||||
- [skills](https://github.com/PicGo/skills): official Agent Skills that teach AI to upload with PicGo.
|
||||
- [dsh-plugin](https://github.com/PicGo/dsh-plugin): PicGo plugin for DeepSeek Harness.
|
||||
|
||||
## Sponsorship
|
||||
|
||||
|
||||
+4
-19
@@ -1,11 +1,12 @@
|
||||
<div align="center" markdown="1">
|
||||
<sup>Special thanks to:</sup>
|
||||
<br>
|
||||
<a href="https://www.nocobase.com/?utm_source=picgo">
|
||||
<img alt="NocoBase sponsorship" width="400" src="https://static-docs.nocobase.com/Logo-Black.png">
|
||||
<a href="https://go.warp.dev/picgo">
|
||||
<img alt="Warp sponsorship" width="400" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-03.png">
|
||||
</a>
|
||||
|
||||
### [NocoBase, AI + No-Code Build reliable business systems](https://www.nocobase.com/?utm_source=picgo)
|
||||
### [Warp, the intelligent terminal for developers](https://go.warp.dev/picgo)
|
||||
[Available for macOS, Linux, & Windows](https://go.warp.dev/picgo)<br>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -91,20 +92,6 @@ PicGo 打造了全方位的上传体验,让“传图”这件事变得前所
|
||||
- **更多可能**:支持图片压缩、水印、重命名、Markdown 图片迁移等功能插件。
|
||||
- 探索更多插件:[Awesome-PicGo](https://github.com/PicGo/Awesome-PicGo)
|
||||
|
||||
### 🤖 AI 友好
|
||||
写文档的活儿越来越多地交给了 AI。但 AI 生成的截图、图表存在本地,``,发布前需要转成可访问链接。PicGo 让 AI 也能轻松传图:
|
||||
|
||||
- **[PicGo Skills](https://github.com/PicGo/skills)**:官方 Agent Skills 集合,教会 AI 什么时候该传图、怎么传。适用于任何支持 skills 格式的 AI 工具。
|
||||
```bash
|
||||
npx skills@latest add PicGo/skills
|
||||
```
|
||||
- **[DeepSeek Harness 插件](https://github.com/PicGo/dsh-plugin)**:在 [dsh](https://github.com/deepseek-ai/deepseek-harness) 里装上它,AI 就能自己把图传到你的图床。
|
||||
```bash
|
||||
dsh plugin --profile web add @picgo/dsh-plugin
|
||||
```
|
||||
|
||||
两者都会复用你在 PicGo 里已经配好的图床和插件,不用重新配置一遍。详见[这篇介绍](https://picgo.app/blog/2026/picgo-deepseek-harness-plugin/)。
|
||||
|
||||
### 🛠 开发者友好
|
||||
- **HTTP API**:支持通过 HTTP 请求调用 PicGo 上传 (v2.2.0+),方便与其他工具集成。
|
||||
- **开源透明**:代码完全开源,安全可靠。
|
||||
@@ -176,8 +163,6 @@ pnpm run build
|
||||
- [vs-picgo](https://github.com/PicGo/vs-picgo):PicGo 的 VS Code 版。
|
||||
- [flutter-picgo](https://github.com/PicGo/flutter-picgo):PicGo 的手机版 App(支持 Android 和 iOS )。
|
||||
- [PicHoro](https://github.com/Kuingsmile/PicHoro):另一款支持 PicGo 配置的手机版 App(暂时只支持 Android)。
|
||||
- [skills](https://github.com/PicGo/skills):PicGo 官方 Agent Skills,让 AI 学会用 PicGo 传图。
|
||||
- [dsh-plugin](https://github.com/PicGo/dsh-plugin):PicGo 的 DeepSeek Harness 插件。
|
||||
|
||||
## 赞助
|
||||
|
||||
|
||||
+8
-4
@@ -99,10 +99,9 @@
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-s3": "^3.276.0",
|
||||
"@aws-sdk/lib-storage": "^3.276.0",
|
||||
"@commitlint/cli": "^21.2.2",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@molunerfinn/vite-plugin-electron-renderer": "^0.14.7",
|
||||
"@picgo/bump-version": "^3.0.0",
|
||||
"@picgo/bump-version": "^2.0.0",
|
||||
"@stylistic/eslint-plugin": "^5.6.1",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
@@ -161,7 +160,7 @@
|
||||
},
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
"./node_modules/@picgo/bump-version/commitlint-picgo/index.cjs"
|
||||
"./node_modules/@picgo/bump-version/commitlint-picgo"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
@@ -169,7 +168,12 @@
|
||||
"path": "./node_modules/cz-customizable"
|
||||
},
|
||||
"cz-customizable": {
|
||||
"config": "./node_modules/@picgo/bump-version/.cz-config.cjs"
|
||||
"config": "./node_modules/@picgo/bump-version/.cz-config.js"
|
||||
}
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"commit-msg": "npm run lint:dpdm && commitlint -E HUSKY_GIT_PARAMS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+770
-523
File diff suppressed because it is too large
Load Diff
+2
-10
@@ -1,11 +1,3 @@
|
||||
allowBuilds:
|
||||
core-js: true
|
||||
electron: true
|
||||
electron-winstaller: true
|
||||
esbuild: true
|
||||
msw: true
|
||||
vue-demi: true
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- core-js
|
||||
- ejs
|
||||
@@ -16,5 +8,5 @@ onlyBuiltDependencies:
|
||||
- vue-demi
|
||||
|
||||
supportedArchitectures:
|
||||
cpu: [ x64, arm64 ]
|
||||
os: [ current ]
|
||||
cpu: [x64, arm64]
|
||||
os: [current]
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
const fs = require('fs')
|
||||
const http = require('http')
|
||||
const https = require('https')
|
||||
const path = require('path')
|
||||
|
||||
const API_URL = process.env.ATOM_API_URL || 'https://api.atomgit.com/api/v5'
|
||||
const ATOM_PAT = process.env.ATOM_PAT
|
||||
const ATOM_OWNER = process.env.ATOM_OWNER
|
||||
const ATOM_REPO = process.env.ATOM_REPO
|
||||
const RELEASE_TAG = process.env.RELEASE_TAG
|
||||
const TARGET_COMMITISH = process.env.ATOM_TARGET_COMMITISH
|
||||
const DIST_DIR = path.resolve(process.env.DIST_DIR || 'dist')
|
||||
const RELEASE_METADATA_PATH = path.resolve(process.env.GITHUB_RELEASE_JSON || 'github-release.json')
|
||||
const MAX_ATTEMPTS = 3
|
||||
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504])
|
||||
const UPLOAD_TIMEOUT_MINUTES = Number.parseInt(process.env.ATOM_UPLOAD_TIMEOUT_MINUTES || '70', 10)
|
||||
|
||||
function requireEnvironmentVariable(name, value) {
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
function encodePathSegment(value) {
|
||||
return encodeURIComponent(value)
|
||||
}
|
||||
|
||||
function wait(milliseconds) {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function formatDuration(milliseconds) {
|
||||
return `${Math.round(milliseconds / 1000)}s`
|
||||
}
|
||||
|
||||
async function requestWithRetry(url, createOptions, label) {
|
||||
let lastError
|
||||
|
||||
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(url, createOptions())
|
||||
if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt === MAX_ATTEMPTS) {
|
||||
return response
|
||||
}
|
||||
|
||||
await response.text()
|
||||
console.warn(`[AtomGit] ${label} returned ${response.status}; retrying (${attempt}/${MAX_ATTEMPTS})`)
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
if (attempt === MAX_ATTEMPTS) {
|
||||
throw error
|
||||
}
|
||||
|
||||
console.warn(`[AtomGit] ${label} failed; retrying (${attempt}/${MAX_ATTEMPTS})`)
|
||||
}
|
||||
|
||||
await wait(1000 * attempt)
|
||||
}
|
||||
|
||||
throw lastError || new Error(`${label} failed after ${MAX_ATTEMPTS} attempts`)
|
||||
}
|
||||
|
||||
function atomGitApiUrl(relativePath) {
|
||||
return `${API_URL}/repos/${encodePathSegment(ATOM_OWNER)}/${encodePathSegment(ATOM_REPO)}${relativePath}`
|
||||
}
|
||||
|
||||
function atomGitHeaders(additionalHeaders = {}) {
|
||||
return {
|
||||
Authorization: `Bearer ${ATOM_PAT}`,
|
||||
Accept: 'application/json',
|
||||
...additionalHeaders
|
||||
}
|
||||
}
|
||||
|
||||
function parseResponseText(text) {
|
||||
if (!text) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
async function readResponseBody(response) {
|
||||
return parseResponseText(await response.text())
|
||||
}
|
||||
|
||||
function formatResponseBody(body) {
|
||||
return typeof body === 'string' ? body : JSON.stringify(body)
|
||||
}
|
||||
|
||||
async function assertSuccessfulResponse(response, label) {
|
||||
const body = await readResponseBody(response)
|
||||
if (!response.ok) {
|
||||
throw new Error(`${label} failed with HTTP ${response.status}: ${formatResponseBody(body)}`)
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
async function verifyCredentials() {
|
||||
const response = await requestWithRetry(
|
||||
`${API_URL}/user`,
|
||||
() => ({ headers: atomGitHeaders() }),
|
||||
'credential verification'
|
||||
)
|
||||
await assertSuccessfulResponse(response, 'AtomGit credential verification')
|
||||
}
|
||||
|
||||
async function getRelease() {
|
||||
const response = await requestWithRetry(
|
||||
atomGitApiUrl(`/releases/tags/${encodePathSegment(RELEASE_TAG)}`),
|
||||
() => ({ headers: atomGitHeaders() }),
|
||||
`get release ${RELEASE_TAG}`
|
||||
)
|
||||
|
||||
if (response.status === 404) {
|
||||
await response.text()
|
||||
return null
|
||||
}
|
||||
|
||||
return assertSuccessfulResponse(response, `Get AtomGit release ${RELEASE_TAG}`)
|
||||
}
|
||||
|
||||
async function createRelease(metadata) {
|
||||
const name = metadata.name || RELEASE_TAG
|
||||
const payload = {
|
||||
tag_name: RELEASE_TAG,
|
||||
name,
|
||||
body: metadata.body || `Release ${name}`,
|
||||
release_status: metadata.isPrerelease ? 'pre' : 'latest'
|
||||
}
|
||||
const targetCommitish = TARGET_COMMITISH || metadata.targetCommitish
|
||||
|
||||
if (targetCommitish) {
|
||||
payload.target_commitish = targetCommitish
|
||||
}
|
||||
|
||||
const response = await requestWithRetry(
|
||||
atomGitApiUrl('/releases'),
|
||||
() => ({
|
||||
method: 'POST',
|
||||
headers: atomGitHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(payload)
|
||||
}),
|
||||
`create release ${RELEASE_TAG}`
|
||||
)
|
||||
|
||||
if (response.status === 409) {
|
||||
await response.text()
|
||||
return getRelease()
|
||||
}
|
||||
|
||||
return assertSuccessfulResponse(response, `Create AtomGit release ${RELEASE_TAG}`)
|
||||
}
|
||||
|
||||
async function getOrCreateRelease(metadata) {
|
||||
const existingRelease = await getRelease()
|
||||
if (existingRelease) {
|
||||
console.log(`[AtomGit] Release ${RELEASE_TAG} already exists`)
|
||||
return existingRelease
|
||||
}
|
||||
|
||||
console.log(`[AtomGit] Creating release ${RELEASE_TAG}`)
|
||||
const createdRelease = await createRelease(metadata)
|
||||
if (!createdRelease) {
|
||||
throw new Error(`AtomGit release ${RELEASE_TAG} was not available after creation`)
|
||||
}
|
||||
|
||||
return createdRelease
|
||||
}
|
||||
|
||||
async function getUploadRequest(fileName) {
|
||||
const url = new URL(atomGitApiUrl(`/releases/${encodePathSegment(RELEASE_TAG)}/upload_url`))
|
||||
url.searchParams.set('file_name', fileName)
|
||||
|
||||
const response = await requestWithRetry(
|
||||
url,
|
||||
() => ({ headers: atomGitHeaders() }),
|
||||
`request upload URL for ${fileName}`
|
||||
)
|
||||
const body = await assertSuccessfulResponse(response, `Get upload URL for ${fileName}`)
|
||||
|
||||
if (!body || typeof body !== 'object' || typeof body.url !== 'string' || !body.headers || typeof body.headers !== 'object') {
|
||||
throw new Error(`AtomGit returned an invalid upload request for ${fileName}`)
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
function putAssetBuffer(url, headers, fileBuffer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const targetUrl = new URL(url)
|
||||
const requestClient = targetUrl.protocol === 'http:' ? http : https
|
||||
const request = requestClient.request(targetUrl, {
|
||||
method: 'PUT',
|
||||
headers: Object.fromEntries(headers.entries())
|
||||
}, response => {
|
||||
const chunks = []
|
||||
|
||||
response.on('data', chunk => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
})
|
||||
response.on('end', () => {
|
||||
resolve({
|
||||
status: response.statusCode || 0,
|
||||
body: Buffer.concat(chunks).toString('utf8')
|
||||
})
|
||||
})
|
||||
response.on('error', reject)
|
||||
})
|
||||
|
||||
request.setTimeout(UPLOAD_TIMEOUT_MINUTES * 60 * 1000, () => {
|
||||
request.destroy(new Error(`Upload request timed out after ${UPLOAD_TIMEOUT_MINUTES} minutes`))
|
||||
})
|
||||
request.on('error', reject)
|
||||
request.end(fileBuffer)
|
||||
})
|
||||
}
|
||||
|
||||
async function uploadAsset(filePath) {
|
||||
const fileName = path.basename(filePath)
|
||||
const fileBuffer = await fs.promises.readFile(filePath)
|
||||
const fileSize = fileBuffer.byteLength
|
||||
const startedAt = Date.now()
|
||||
|
||||
console.log(`[AtomGit] Uploading ${fileName} (${formatFileSize(fileSize)})`)
|
||||
|
||||
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
|
||||
const uploadRequest = await getUploadRequest(fileName)
|
||||
const uploadHeaders = new Headers(uploadRequest.headers)
|
||||
uploadHeaders.set('Content-Length', String(fileSize))
|
||||
let response
|
||||
|
||||
try {
|
||||
response = await putAssetBuffer(uploadRequest.url, uploadHeaders, fileBuffer)
|
||||
} catch (error) {
|
||||
if (attempt === MAX_ATTEMPTS) {
|
||||
throw error
|
||||
}
|
||||
|
||||
console.warn(`[AtomGit] Upload ${fileName} failed; requesting a new upload URL (${attempt}/${MAX_ATTEMPTS})`)
|
||||
await wait(1000 * attempt)
|
||||
continue
|
||||
}
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
console.log(`[AtomGit] Uploaded ${fileName} in ${formatDuration(Date.now() - startedAt)}`)
|
||||
return
|
||||
}
|
||||
|
||||
const responseBody = parseResponseText(response.body)
|
||||
if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt === MAX_ATTEMPTS) {
|
||||
throw new Error(`Upload ${fileName} failed with HTTP ${response.status}: ${formatResponseBody(responseBody)}`)
|
||||
}
|
||||
|
||||
console.warn(`[AtomGit] Upload ${fileName} returned ${response.status}; requesting a new upload URL (${attempt}/${MAX_ATTEMPTS})`)
|
||||
await wait(1000 * attempt)
|
||||
}
|
||||
}
|
||||
|
||||
function readReleaseMetadata() {
|
||||
const metadata = JSON.parse(fs.readFileSync(RELEASE_METADATA_PATH, 'utf8'))
|
||||
if (metadata.isDraft) {
|
||||
throw new Error(`GitHub release ${RELEASE_TAG} is still a draft and will not be mirrored`)
|
||||
}
|
||||
|
||||
if (metadata.tagName !== RELEASE_TAG) {
|
||||
throw new Error(`GitHub release tag ${metadata.tagName} does not match requested tag ${RELEASE_TAG}`)
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
function findAssets() {
|
||||
return fs.readdirSync(DIST_DIR, { withFileTypes: true })
|
||||
.filter(entry => entry.isFile())
|
||||
.map(entry => path.join(DIST_DIR, entry.name))
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
}
|
||||
|
||||
function getExistingAssetNames(release) {
|
||||
if (!Array.isArray(release.assets)) {
|
||||
return new Set()
|
||||
}
|
||||
|
||||
return new Set(release.assets
|
||||
.map(asset => asset && asset.name)
|
||||
.filter(name => typeof name === 'string'))
|
||||
}
|
||||
|
||||
async function publishRelease() {
|
||||
requireEnvironmentVariable('ATOM_PAT', ATOM_PAT)
|
||||
requireEnvironmentVariable('ATOM_OWNER', ATOM_OWNER)
|
||||
requireEnvironmentVariable('ATOM_REPO', ATOM_REPO)
|
||||
requireEnvironmentVariable('RELEASE_TAG', RELEASE_TAG)
|
||||
if (!Number.isInteger(UPLOAD_TIMEOUT_MINUTES) || UPLOAD_TIMEOUT_MINUTES < 1 || UPLOAD_TIMEOUT_MINUTES > 180) {
|
||||
throw new Error('ATOM_UPLOAD_TIMEOUT_MINUTES must be an integer between 1 and 180')
|
||||
}
|
||||
|
||||
const metadata = readReleaseMetadata()
|
||||
const assetPaths = findAssets()
|
||||
if (assetPaths.length === 0) {
|
||||
throw new Error(`No GitHub release assets found in ${DIST_DIR}`)
|
||||
}
|
||||
|
||||
console.log(`[AtomGit] Mirroring ${assetPaths.length} assets to ${ATOM_OWNER}/${ATOM_REPO}@${RELEASE_TAG}`)
|
||||
await verifyCredentials()
|
||||
const release = await getOrCreateRelease(metadata)
|
||||
const existingAssetNames = getExistingAssetNames(release)
|
||||
|
||||
for (const assetPath of assetPaths) {
|
||||
const fileName = path.basename(assetPath)
|
||||
if (existingAssetNames.has(fileName)) {
|
||||
console.log(`[AtomGit] Skipping existing asset ${fileName}`)
|
||||
continue
|
||||
}
|
||||
|
||||
await uploadAsset(assetPath)
|
||||
}
|
||||
|
||||
console.log(`[AtomGit] Release ${RELEASE_TAG} mirror completed`)
|
||||
}
|
||||
|
||||
publishRelease().catch(error => {
|
||||
console.error('[AtomGit] Publish failed:', error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -11,6 +11,7 @@ const distPath = path.join(__dirname, '../dist')
|
||||
const S3Client = require('@aws-sdk/client-s3').S3Client
|
||||
const Upload = require('@aws-sdk/lib-storage').Upload
|
||||
const uploadToDev = process.argv.includes('--dev')
|
||||
const uploadVersionFilesOnly = process.argv.includes('--version-files-only')
|
||||
|
||||
const S3_BUCKET = 'release'
|
||||
const S3_LEGACY_BUCKET = 'picgo'
|
||||
@@ -103,6 +104,7 @@ const uploadDist = async () => {
|
||||
|
||||
console.log(`[PicGo] Upload mode: ${shouldUploadAll() ? 'ALL PLATFORMS' : process.platform}`)
|
||||
console.log(`[PicGo] Version: ${VERSION}`)
|
||||
console.log(`[PicGo] Install package upload: ${uploadVersionFilesOnly ? 'SKIPPED' : 'ENABLED'}`)
|
||||
console.log(`[PicGo] Total files to upload: ${configs.length}\n`)
|
||||
|
||||
const uploadedVersionFiles = new Set()
|
||||
@@ -117,11 +119,11 @@ const uploadDist = async () => {
|
||||
console.log(`[${index + 1}/${configs.length}] Processing ${fileName}`)
|
||||
|
||||
// 上传构建产物
|
||||
if (fs.existsSync(filePath)) {
|
||||
if (!uploadVersionFilesOnly && fs.existsSync(filePath)) {
|
||||
console.log(` Uploading to S3: ${FILE_PATH}${fileName}`)
|
||||
await uploadFileToS3(client, S3_BUCKET, `${FILE_PATH}${fileName}`, filePath)
|
||||
console.log(` ✅ Uploaded: ${fileName}`)
|
||||
} else {
|
||||
} else if (!uploadVersionFilesOnly) {
|
||||
console.warn(` ⚠️ File not found: ${fileName}`)
|
||||
}
|
||||
|
||||
|
||||
Vendored
-1
@@ -144,7 +144,6 @@ interface IBrowserWindowOptions {
|
||||
nodeIntegration: boolean,
|
||||
nodeIntegrationInWorker: boolean,
|
||||
contextIsolation: boolean,
|
||||
// 不传即沿用 Electron 默认值(true,窗口不可见时降频);仅在某个窗口确实需要后台全速渲染时才显式关闭。
|
||||
backgroundThrottling?: boolean
|
||||
webSecurity?: boolean
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user