Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

125 lines
4.1 KiB
TypeScript

/** Characters that are illegal in file names on common desktop platforms. */
const ILLEGAL_ENTRY_CHARS = /[<>:"\\|?*\x00-\x1f]/g
/** A workspace file to place inside an archive. */
export interface ZipEntrySource {
/** Display name of the file. Only its final path segment is used. */
name: string
/** Workspace-root-relative folder path holding the file, or null when it sits at the root. */
folderPath?: string | null
}
export interface BuildZipEntryPathsOptions {
/**
* Drop the deepest folder path shared by every source, so an archive built from
* one folder's contents is not buried under that folder's ancestor chain.
*/
rebaseOnCommonFolder?: boolean
}
/** Split a folder path into non-empty segments. */
function toSegments(folderPath?: string | null): string[] {
return folderPath ? folderPath.split('/').filter(Boolean) : []
}
/**
* Reduce a name to a safe, single path segment: takes the final segment, drops
* directory and traversal components, and falls back when nothing usable remains.
* Keeps untrusted names from introducing nested or zip-slip-style paths.
*/
function toLeafName(name: string): string {
const leaf = name.replace(/\\/g, '/').split('/').pop()?.trim()
if (!leaf || leaf === '.' || leaf === '..') return 'file'
return leaf
}
/**
* Sanitize a `/`-joined entry path segment by segment: strips characters that are
* illegal on common desktop platforms, neutralizes `.`/`..` traversal segments, and
* drops empty segments. Returns `''` when no usable segment remains.
*/
function safeEntryPath(path: string): string {
return path
.split('/')
.map((segment) => {
const cleaned = segment.trim().replace(ILLEGAL_ENTRY_CHARS, '_')
return cleaned === '.' || cleaned === '..' ? '_' : cleaned
})
.filter(Boolean)
.join('/')
}
/**
* Append a numeric suffix to the file name of an entry path, leaving the directory
* portion intact (`docs/report.pdf` -> `docs/report (1).pdf`).
*/
function withEntrySuffix(path: string, suffix: number): string {
const slashIndex = path.lastIndexOf('/')
const directory = path.slice(0, slashIndex + 1)
const fileName = path.slice(slashIndex + 1)
const dotIndex = fileName.lastIndexOf('.')
return dotIndex > 0
? `${directory}${fileName.slice(0, dotIndex)} (${suffix})${fileName.slice(dotIndex)}`
: `${directory}${fileName} (${suffix})`
}
/**
* Longest folder path shared by every source, compared segment by segment so that
* sibling folders like `Reports` and `Reports-2026` are never treated as nested.
* Returns `''` when the sources have no common folder.
*/
function commonFolderSegments(sources: ZipEntrySource[]): string[] {
let common: string[] | null = null
for (const source of sources) {
const segments = toSegments(source.folderPath)
if (common === null) {
common = segments
continue
}
let matched = 0
while (
matched < common.length &&
matched < segments.length &&
common[matched] === segments[matched]
) {
matched++
}
common = common.slice(0, matched)
if (common.length === 0) break
}
return common ?? []
}
/**
* Build unique, traversal-safe zip entry paths that mirror the workspace folder
* layout of `sources`. Returns one path per source, in the same order.
*
* Collisions get a numeric suffix before the extension, matching the workspace
* file naming convention (`report.pdf` -> `report (1).pdf`).
*/
export function buildZipEntryPaths(
sources: ZipEntrySource[],
options?: BuildZipEntryPathsOptions
): string[] {
const rebaseLength = options?.rebaseOnCommonFolder ? commonFolderSegments(sources).length : 0
const usedPaths = new Set<string>()
return sources.map((source) => {
const leafName = toLeafName(source.name)
const folderSegments = toSegments(source.folderPath).slice(rebaseLength)
const basePath = safeEntryPath([...folderSegments, leafName].join('/')) || leafName
let candidate = basePath
let suffix = 1
while (usedPaths.has(candidate)) {
candidate = withEntrySuffix(basePath, suffix)
suffix++
}
usedPaths.add(candidate)
return candidate
})
}