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
125 lines
4.1 KiB
TypeScript
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
|
|
})
|
|
}
|