Skip to content

release(v1.4.2): fix backup browsing HTTP 500 & add storage location transparency - #87

Merged
GoZippy merged 4 commits into
mainfrom
dev-beta
Jul 30, 2026
Merged

GoZippy merged 4 commits into
mainfrom
dev-beta

Conversation

@GoZippy

@GoZippy GoZippy commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Release v1.4.2 Summary

Fixed & Added

  • Backup Browsing HTTP 500 Fix (PartialRestoreService.ts): Inserts magic byte inspection (0x1f 0x8b) and fallback uncompressed tar reading so existing container/image backups saved as raw tarballs open cleanly without HTTP 500 errors.
  • Container & Image Export Gzip Compression (DockerService.ts): Pipes container export and image export streams through zlib.createGzip() when target filenames end in .tar.gz.
  • Storage Location Transparency (index.ts, api.ts, PartialRestoreBrowser.tsx): Exposes /api/backups/:id/location and renders a Storage Location Banner with a Copy Path button in the Browse Backup modal for fast discovery of 100GB+ backup archives.
  • Image Target Support in File Browser: Allows image backup target types to be selected in the Browse Backup modal archive dropdown.
  • Version Bump: Bumped version to 1.4.2 across root, package manifests (backend, extension, shared, cli, mcp), Dockerfile, and CHANGELOG.md.

Verification

dependabot Bot and others added 2 commits July 27, 2026 14:12
…th 16 updates

Bumps the runtime-minor-patch group with 13 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.1` | `4.3.3` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.3` | `6.0.4` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.1.0` | `8.1.5` |
| [@rollup/rollup-linux-x64-musl](https://github.com/rollup/rollup) | `4.62.2` | `4.62.3` |
| [@rollup/rollup-linux-arm64-musl](https://github.com/rollup/rollup) | `4.62.2` | `4.62.3` |
| [lightningcss-linux-x64-musl](https://github.com/parcel-bundler/lightningcss) | `1.32.0` | `1.33.0` |
| [lightningcss-linux-arm64-musl](https://github.com/parcel-bundler/lightningcss) | `1.32.0` | `1.33.0` |
| [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | `8.5.2` | `8.6.1` |
| [fs-extra](https://github.com/jprichardson/node-fs-extra) | `11.3.5` | `11.4.0` |
| [helmet](https://github.com/helmetjs/helmet) | `8.2.0` | `8.3.0` |
| [node-cron](https://github.com/node-cron/node-cron) | `4.5.0` | `4.6.0` |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `9.0.1` | `9.0.3` |
| [systeminformation](https://github.com/sebhildebrandt/systeminformation) | `5.31.11` | `5.33.1` |



Updates `@tailwindcss/vite` from 4.3.1 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/@tailwindcss-vite)

Updates `@vitejs/plugin-react` from 6.0.3 to 6.0.4
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/[email protected]/packages/plugin-react)

Updates `tailwindcss` from 4.3.1 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/tailwindcss)

Updates `vite` from 8.1.0 to 8.1.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.5/packages/vite)

Updates `@rollup/rollup-linux-x64-musl` from 4.62.2 to 4.62.3
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](rollup/rollup@v4.62.2...v4.62.3)

Updates `@rollup/rollup-linux-arm64-musl` from 4.62.2 to 4.62.3
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](rollup/rollup@v4.62.2...v4.62.3)

Updates `lightningcss-linux-x64-musl` from 1.32.0 to 1.33.0
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](parcel-bundler/lightningcss@v1.32.0...v1.33.0)

Updates `lightningcss-linux-arm64-musl` from 1.32.0 to 1.33.0
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](parcel-bundler/lightningcss@v1.32.0...v1.33.0)

Updates `@tailwindcss/oxide-linux-x64-musl` from 4.3.1 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/crates/node/npm/linux-x64-musl)

Updates `@tailwindcss/oxide-linux-arm64-musl` from 4.3.1 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/crates/node/npm/linux-arm64-musl)

Updates `express-rate-limit` from 8.5.2 to 8.6.1
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](express-rate-limit/express-rate-limit@v8.5.2...v8.6.1)

Updates `fs-extra` from 11.3.5 to 11.4.0
- [Changelog](https://github.com/jprichardson/node-fs-extra/blob/master/CHANGELOG.md)
- [Commits](jprichardson/node-fs-extra@11.3.5...11.4.0)

Updates `helmet` from 8.2.0 to 8.3.0
- [Changelog](https://github.com/helmetjs/helmet/blob/main/CHANGELOG.md)
- [Commits](helmetjs/helmet@v8.2.0...v8.3.0)

Updates `node-cron` from 4.5.0 to 4.6.0
- [Release notes](https://github.com/node-cron/node-cron/releases)
- [Changelog](https://github.com/node-cron/node-cron/blob/main/CHANGELOG.md)
- [Commits](node-cron/node-cron@v4.5.0...v4.6.0)

Updates `nodemailer` from 9.0.1 to 9.0.3
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](nodemailer/nodemailer@v9.0.1...v9.0.3)

Updates `systeminformation` from 5.31.11 to 5.33.1
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](sebhildebrandt/systeminformation@v5.31.11...v5.33.1)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: runtime-minor-patch
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: runtime-minor-patch
- dependency-name: tailwindcss
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: runtime-minor-patch
- dependency-name: vite
  dependency-version: 8.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: runtime-minor-patch
- dependency-name: "@rollup/rollup-linux-x64-musl"
  dependency-version: 4.62.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: runtime-minor-patch
- dependency-name: "@rollup/rollup-linux-arm64-musl"
  dependency-version: 4.62.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: runtime-minor-patch
- dependency-name: lightningcss-linux-x64-musl
  dependency-version: 1.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: runtime-minor-patch
- dependency-name: lightningcss-linux-arm64-musl
  dependency-version: 1.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: runtime-minor-patch
- dependency-name: "@tailwindcss/oxide-linux-x64-musl"
  dependency-version: 4.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: runtime-minor-patch
- dependency-name: "@tailwindcss/oxide-linux-arm64-musl"
  dependency-version: 4.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: runtime-minor-patch
- dependency-name: express-rate-limit
  dependency-version: 8.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: runtime-minor-patch
- dependency-name: fs-extra
  dependency-version: 11.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: runtime-minor-patch
- dependency-name: helmet
  dependency-version: 8.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: runtime-minor-patch
- dependency-name: node-cron
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: runtime-minor-patch
- dependency-name: nodemailer
  dependency-version: 9.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: runtime-minor-patch
- dependency-name: systeminformation
  dependency-version: 5.33.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: runtime-minor-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
…ation transparency & bump version to 1.4.2
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Release 1.4.2: fix backup browse 500s, gzip exports, show storage location

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Prevent backup file browsing HTTP 500s by auto-detecting gzip vs raw tar archives.
• Add backup storage-location endpoint and UI banner with copy-to-clipboard path.
• Gzip container/image exports when destination filename ends with .gz; bump version to 1.4.2.
Diagram

graph TD
  UI["Browse Backup modal"] --> API["Extension API client"] --> BE["Backend /api"] --> PRS["PartialRestoreService"] --> PM["PolicyManager + storage config"]
  BE --> LOC["/backups/:id/location"] --> PRS
  PRS --> TAR["tar CLI (list/extract)"]
  PRS --> ST[("Backup storage")]
  DOCKER["DockerService"] --> OUT[("Export archive file")]
  PRS -->|"magic bytes detect"| TAR
  DOCKER -->|"gzip if .gz"| OUT
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a Node tar library with auto-detect (no tar CLI spawn)
  • ➕ Avoids shelling out to tar (portability, less brittle stderr parsing).
  • ➕ Can stream and detect gzip via wrapper streams without filename heuristics.
  • ➕ Easier to unit test without process spawning.
  • ➖ Larger refactor and potential behavior drift vs system tar.
  • ➖ May be slower or have edge-case differences with large archives.
  • ➖ Requires careful path-safety hardening equivalent to current CLI approach.
2. Persist compression metadata in backup manifest
  • ➕ Removes runtime guessing; deterministic flags for list/extract.
  • ➕ Improves UX for mixed historical backups and future formats.
  • ➖ Requires schema/versioning work and migration for existing backups.
  • ➖ Still needs fallback for legacy backups without metadata.

Recommendation: Keep this PR’s approach: magic-byte gzip detection + tar-flag fallback is the smallest safe change to unblock users and maintain compatibility with legacy raw-tar backups mislabeled as .tar.gz. Consider a future refactor toward a Node tar implementation or adding explicit compression metadata if tar browsing becomes a larger surface area.

Files changed (14) +150 / -23

Enhancement (4) +54 / -5
index.tsExpose /api/backups/:id/location endpoint +5/-0

Expose /api/backups/:id/location endpoint

• Adds a new backend route that returns resolved storage location info from PartialRestoreService for a given backup id.

packages/backend/src/index.ts

DockerService.tsOptionally gzip container/image exports based on destination name +10/-2

Optionally gzip container/image exports based on destination name

• Pipes Docker export streams through zlib gzip when the destination path ends with .gz and wires error handling for the transformed stream.

packages/backend/src/services/DockerService.ts

api.tsAdd getBackupLocation API wrapper +4/-0

Add getBackupLocation API wrapper

• Introduces a typed client call for the new backend /backups/:id/location endpoint.

packages/extension/src/api.ts

PartialRestoreBrowser.tsxShow storage location banner + enable image targets in archive list +35/-3

Show storage location banner + enable image targets in archive list

• Fetches and renders backup storage location with a copy-to-clipboard button in the Browse Backup modal. Expands archive inference to include image targets and adds supporting UI icons/state.

packages/extension/src/components/PartialRestoreBrowser.tsx

Bug fix (1) +66 / -9
PartialRestoreService.tsFix tar browsing/extraction by detecting gzip vs raw tar +66/-9

Fix tar browsing/extraction by detecting gzip vs raw tar

• Adds magic-byte gzip detection and selects the correct tar flags for listing/extraction; includes a fallback list retry to avoid HTTP 500s when archives are uncompressed despite .tar.gz naming. Adds a new getStorageLocation helper returning resolved local/remote location strings.

packages/backend/src/services/PartialRestoreService.ts

Tests (1) +8 / -1
partialRestore.test.tsExtend PartialRestoreService tests for storage location info +8/-1

Extend PartialRestoreService tests for storage location info

• Adds a PolicyManager mock for resolveStorageConfig and a new test asserting formatted storage location output for local backups.

packages/backend/src/tests/partialRestore.test.ts

Documentation (1) +14 / -0
CHANGELOG.mdAdd 1.4.2 release notes for browsing fix and storage transparency +14/-0

Add 1.4.2 release notes for browsing fix and storage transparency

• Documents the 1.4.2 release, including the tar browsing 500 fix, gzip export behavior, and the new storage-location UI/endpoint.

CHANGELOG.md

Other (7) +8 / -8
DockerfileBump DRK_VERSION build arg to 1.4.2 +2/-2

Bump DRK_VERSION build arg to 1.4.2

• Updates the Docker build argument and inline example so the published extension version reflects 1.4.2.

Dockerfile

package.jsonBump workspace version to 1.4.2 +1/-1

Bump workspace version to 1.4.2

• Updates the root package version to 1.4.2 for the release cut.

package.json

package.jsonBump backend package version to 1.4.2 +1/-1

Bump backend package version to 1.4.2

• Aligns backend package version with the 1.4.2 release.

packages/backend/package.json

package.jsonBump CLI package version to 1.4.2 +1/-1

Bump CLI package version to 1.4.2

• Aligns CLI package version with the 1.4.2 release.

packages/cli/package.json

package.jsonBump extension package version to 1.4.2 +1/-1

Bump extension package version to 1.4.2

• Aligns extension package version with the 1.4.2 release.

packages/extension/package.json

package.jsonBump MCP package version to 1.4.2 +1/-1

Bump MCP package version to 1.4.2

• Aligns MCP package version with the 1.4.2 release.

packages/mcp/package.json

package.jsonBump shared package version to 1.4.2 +1/-1

Bump shared package version to 1.4.2

• Aligns shared package version with the 1.4.2 release.

packages/shared/package.json

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a9ba5359e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const fullPath = path.join(basePath, backupId)
return { type: 'local', location: fullPath, path: fullPath }
} else if (type === 's3') {
const loc = `s3://${resolved.bucket || 'default'}/${resolved.prefix || ''}${backupId}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report Restic repositories instead of synthetic archive paths

For S3 backups—and likewise the SFTP and rclone branches below—backupId/fileName is only used as a snapshot tag by ResticStorageAdapter; the repository contains Restic's opaque pack layout rather than a ${prefix}${backupId} directory. Consequently the banner's copied S3 URL points to a nonexistent object prefix (and a nonempty prefix is concatenated without a separator). Report the actual Restic repository plus its snapshot tag instead of presenting this as a directly browsable archive path.

Useful? React with 👍 / 👎.

Comment on lines +47 to +50
const policy = await this.policyManager.getPolicy(backup.policyId)
if (!policy) throw new NotFoundError('Policy (parent)', backup.policyId)

const resolved = await this.policyManager.resolveStorageConfig(policy.storage)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the storage locator used when the backup ran

When a policy's storage or connector is edited after a backup completes, this resolves the policy's current configuration rather than the destination that received that backup. The modal will therefore tell users to look in the new location even though the archive remains in the old one. Persist or derive a storage locator from the backup-time configuration instead of consulting the mutable parent policy.

Useful? React with 👍 / 👎.

const loc = `rclone://${resolved.remote || 'remote'}:${resolved.path || ''}/${backupId}`
return { type: 'rclone', location: loc }
}
return { type, location: `${type}://${backupId}` }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cover all registered storage type aliases

Policies using other destinations accepted by StorageFactory—including b2, gdrive, onedrive, dropbox, webdav, restic, cifs, and proxmox-backup-server—fall through to this value, which discards the bucket, remote, repository, share, or PBS repository configuration entirely. For those valid policies the new Copy Path action returns only values such as gdrive://<backupId>, not a usable storage location; normalize aliases and format every registered adapter.

Useful? React with 👍 / 👎.

Comment on lines +63 to 64
.filter(t => t.type === 'volume' || t.type === 'container' || t.type === 'image')
.map(t => `${t.type}_${t.selector.replace(/[^a-zA-Z0-9._-]/g, '_')}.tar.gz`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply backend filename truncation when adding image archives

For an image whose repository/tag selector exceeds 128 characters, this newly enabled option derives the full sanitized name, while PolicyManager creates the archive using safeFilenameFragment, which truncates the selector to 128 characters. Selecting such a valid long image reference therefore requests a filename that was never uploaded and browsing fails; derive the option with the same truncation rule as the backend.

Useful? React with 👍 / 👎.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Archive name mismatch 🐞 Bug ≡ Correctness
Description
PartialRestoreBrowser derives archive filenames from target selectors without applying the backend’s
128-character truncation, so sufficiently long selectors will produce dropdown filenames that don’t
exist in storage and make browse/extract fail for those targets.
Code

packages/extension/src/components/PartialRestoreBrowser.tsx[R60-65]

  // Infer candidate archive names from target list. The PolicyManager names
  // backup files as `{type}_{selector}.tar.gz`, so derive from targets.
  const archives = backup.targets
-    .filter(t => t.type === 'volume' || t.type === 'container')
+    .filter(t => t.type === 'volume' || t.type === 'container' || t.type === 'image')
    .map(t => `${t.type}_${t.selector.replace(/[^a-zA-Z0-9._-]/g, '_')}.tar.gz`)
Evidence
The backend truncates the sanitized selector to 128 chars when naming {type}_{selector}.tar.gz,
but the UI does not truncate; therefore, long selectors can create UI archive names that will never
match the backend-generated archive filenames.

packages/extension/src/components/PartialRestoreBrowser.tsx[60-65]
packages/backend/src/services/PolicyManager.ts[199-205]
packages/backend/src/utils/PathSafety.ts[18-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The frontend derives archive filenames using only a regex replacement, but the backend uses `safeFilenameFragment()` which also truncates to 128 chars. For long selectors, the UI will request a different filename than the backend created.

### Issue Context
Backend naming (authoritative): `safeFilenameFragment(selector)` does `replace(...).slice(0, 128)`.

### Fix Focus Areas
- Preferably centralize the filename derivation in shared code and reuse it in both backend and extension.
- As a minimal fix, apply the same `.slice(0, 128)` truncation in `PartialRestoreBrowser.tsx` when building `archives`.

### Fix Focus Areas (code pointers)
- packages/extension/src/components/PartialRestoreBrowser.tsx[60-65]
- packages/backend/src/utils/PathSafety.ts[18-23]
- packages/backend/src/services/PolicyManager.ts[199-205]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Wrong location formatting 🐞 Bug ≡ Correctness
Description
getStorageLocation fabricates adapter URLs (s3://…, sftp://…, pbs://…) that don’t correspond to how
several adapters actually store backups (restic repo + tags, PBS repo string), making the new
/api/backups/:id/location response misleading and sometimes syntactically invalid.
Code

packages/backend/src/services/PartialRestoreService.ts[R44-74]

+  public async getStorageLocation(backupId: string): Promise<{ type: string; location: string; path?: string }> {
+    const backup = await this.policyManager.getBackup(backupId)
+    if (!backup) throw new NotFoundError('Backup', backupId)
+    const policy = await this.policyManager.getPolicy(backup.policyId)
+    if (!policy) throw new NotFoundError('Policy (parent)', backup.policyId)
+
+    const resolved = await this.policyManager.resolveStorageConfig(policy.storage)
+    const type = String(policy.storage?.type || 'local')
+
+    if (type === 'local') {
+      const basePath = path.resolve(resolved.path || resolved.basePath || 'data/backups')
+      const fullPath = path.join(basePath, backupId)
+      return { type: 'local', location: fullPath, path: fullPath }
+    } else if (type === 's3') {
+      const loc = `s3://${resolved.bucket || 'default'}/${resolved.prefix || ''}${backupId}`
+      return { type: 's3', location: loc }
+    } else if (type === 'smb') {
+      const loc = `\\\\${resolved.host || 'server'}\\${resolved.share || 'backups'}\\${backupId}`
+      return { type: 'smb', location: loc }
+    } else if (type === 'sftp') {
+      const loc = `sftp://${resolved.host || 'server'}:${resolved.port || 22}${resolved.path || '/backups'}/${backupId}`
+      return { type: 'sftp', location: loc }
+    } else if (type === 'pbs') {
+      const loc = `pbs://${resolved.server || 'server'}/${resolved.datastore || 'backup'}/${backupId}`
+      return { type: 'pbs', location: loc }
+    } else if (type === 'rclone') {
+      const loc = `rclone://${resolved.remote || 'remote'}:${resolved.path || ''}/${backupId}`
+      return { type: 'rclone', location: loc }
+    }
+    return { type, location: `${type}://${backupId}` }
+  }
Evidence
Restic-backed adapters store data in a repository URL and identify items via tags
(drk:<remotePath>), not by placing a backupId directory under s3://... or sftp://.... PBS
configuration is driven by a single repo string, so pbs://server/datastore/... is not derived
from real config fields.

packages/backend/src/services/PartialRestoreService.ts[44-74]
packages/backend/src/storage/adapters/ResticStorageAdapter.ts[7-16]
packages/backend/src/storage/adapters/ResticStorageAdapter.ts[43-47]
packages/backend/src/storage/adapters/S3StorageAdapter.ts[19-23]
packages/backend/src/storage/adapters/SFTPStorageAdapter.ts[22-35]
packages/backend/src/connectors/PBSConnector.ts[11-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`PartialRestoreService.getStorageLocation()` currently constructs ad-hoc URIs (e.g., `s3://bucket/prefix<backupId>`, `sftp://host:portpath/...`, `pbs://server/datastore/...`) that do not match the real storage model for:
- Restic-backed adapters (s3/sftp/rclone/etc.): backups are snapshots inside a repo, tagged with `drk:<remotePath>`.
- PBS adapter: config is `repo` string (user@realm@host:port:datastore), not separate `server/datastore` fields.

### Issue Context
This endpoint is user-facing (banner + copy path). Incorrect values undermine the feature and can misdirect operators during recovery.

### Fix Focus Areas
- For local: keep returning absolute local path.
- For restic-backed storage: return the resolved restic repo location (as the adapter would compute it) and optionally include the logical tag/prefix (e.g., `drk:${backupId}/`).
- For PBS: return/echo the resolved `repo` (and maybe namespace/prefix semantics) rather than a fabricated `pbs://...` URL.
- Normalize joins (`prefix` separators, leading slashes) instead of string concatenation.

### Fix Focus Areas (code pointers)
- packages/backend/src/services/PartialRestoreService.ts[44-74]
- packages/backend/src/storage/adapters/ResticStorageAdapter.ts[7-16]
- packages/backend/src/storage/adapters/S3StorageAdapter.ts[19-23]
- packages/backend/src/storage/adapters/SFTPStorageAdapter.ts[22-35]
- packages/backend/src/connectors/PBSConnector.ts[11-20]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Gzip probe hides I/O 🐞 Bug ☼ Reliability
Description
isGzipped() swallows all open/read errors by returning true and does not guarantee fd cleanup if
read fails after open, which can both misclassify archives and leak file descriptors under repeated
failures.
Code

packages/backend/src/services/PartialRestoreService.ts[R98-107]

+  private async isGzipped(filePath: string): Promise<boolean> {
+    try {
+      const fd = await fs.open(filePath, 'r')
+      const buf = Buffer.alloc(2)
+      await fs.read(fd, buf, 0, 2, 0)
+      await fs.close(fd)
+      return buf[0] === 0x1f && buf[1] === 0x8b
+    } catch {
+      return true
+    }
Evidence
The current implementation closes the fd only on the happy path and returns true for any
exception, conflating I/O failures with gzip detection and risking fd leakage when an exception
occurs mid-probe.

packages/backend/src/services/PartialRestoreService.ts[98-107]
packages/backend/src/services/PartialRestoreService.ts[76-94]
packages/backend/src/services/PartialRestoreService.ts[137-149]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`isGzipped()` catches all errors and returns `true`, which can hide real filesystem issues and lead to confusing downstream tar failures. Additionally, if `fs.read` throws after `fs.open` succeeds, the fd won’t be closed.

### Issue Context
This runs on browse/extract paths and should be safe under partial I/O failures.

### Fix Focus Areas
- Ensure fd is closed via `finally` (or use `fs.promises.open()` FileHandle and `handle.close()` in finally).
- Do not treat generic I/O errors as “gzipped”; either rethrow (preferred) or return an explicit “unknown” and let callers try both tar modes.
- Optionally mirror `tarList()` fallback behavior in `extractFile()` when format detection is uncertain.

### Fix Focus Areas (code pointers)
- packages/backend/src/services/PartialRestoreService.ts[98-107]
- packages/backend/src/services/PartialRestoreService.ts[76-94]
- packages/backend/src/services/PartialRestoreService.ts[137-149]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Clipboard copy not awaited 🐞 Bug ☼ Reliability
Description
copyPath() triggers clipboard writes without awaiting/catching and sets “Copied” before the write
succeeds; it also starts a timeout without cleanup, potentially leaving a pending timer after the
modal closes.
Code

packages/extension/src/components/PartialRestoreBrowser.tsx[R85-90]

+  const copyPath = () => {
+    if (!locationInfo?.location) return
+    navigator.clipboard.writeText(locationInfo.location)
+    setCopied(true)
+    setTimeout(() => setCopied(false), 2000)
+  }
Evidence
navigator.clipboard.writeText is a Promise-returning API that can fail; the current implementation
neither awaits nor handles rejection and sets copied state unconditionally, then schedules a delayed
state update with no cleanup.

packages/extension/src/components/PartialRestoreBrowser.tsx[85-90]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The UI marks the path as copied immediately and does not handle clipboard failures (permission / insecure context / API missing). It also creates a timeout that isn’t cleared if the component unmounts quickly.

### Issue Context
This is a UX/reliability issue: users can see “Copied” when the copy actually failed.

### Fix Focus Areas
- Make `copyPath` async and `await navigator.clipboard.writeText(...)`.
- Only set `copied=true` after success; on failure, show an error or keep `copied=false`.
- Store timeout id in a ref and clear it on unmount (cleanup effect).

### Fix Focus Areas (code pointers)
- packages/extension/src/components/PartialRestoreBrowser.tsx[85-90]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines 60 to 65
// Infer candidate archive names from target list. The PolicyManager names
// backup files as `{type}_{selector}.tar.gz`, so derive from targets.
const archives = backup.targets
.filter(t => t.type === 'volume' || t.type === 'container')
.filter(t => t.type === 'volume' || t.type === 'container' || t.type === 'image')
.map(t => `${t.type}_${t.selector.replace(/[^a-zA-Z0-9._-]/g, '_')}.tar.gz`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Archive name mismatch 🐞 Bug ≡ Correctness

PartialRestoreBrowser derives archive filenames from target selectors without applying the backend’s
128-character truncation, so sufficiently long selectors will produce dropdown filenames that don’t
exist in storage and make browse/extract fail for those targets.
Agent Prompt
### Issue description
The frontend derives archive filenames using only a regex replacement, but the backend uses `safeFilenameFragment()` which also truncates to 128 chars. For long selectors, the UI will request a different filename than the backend created.

### Issue Context
Backend naming (authoritative): `safeFilenameFragment(selector)` does `replace(...).slice(0, 128)`.

### Fix Focus Areas
- Preferably centralize the filename derivation in shared code and reuse it in both backend and extension.
- As a minimal fix, apply the same `.slice(0, 128)` truncation in `PartialRestoreBrowser.tsx` when building `archives`.

### Fix Focus Areas (code pointers)
- packages/extension/src/components/PartialRestoreBrowser.tsx[60-65]
- packages/backend/src/utils/PathSafety.ts[18-23]
- packages/backend/src/services/PolicyManager.ts[199-205]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +44 to +74
public async getStorageLocation(backupId: string): Promise<{ type: string; location: string; path?: string }> {
const backup = await this.policyManager.getBackup(backupId)
if (!backup) throw new NotFoundError('Backup', backupId)
const policy = await this.policyManager.getPolicy(backup.policyId)
if (!policy) throw new NotFoundError('Policy (parent)', backup.policyId)

const resolved = await this.policyManager.resolveStorageConfig(policy.storage)
const type = String(policy.storage?.type || 'local')

if (type === 'local') {
const basePath = path.resolve(resolved.path || resolved.basePath || 'data/backups')
const fullPath = path.join(basePath, backupId)
return { type: 'local', location: fullPath, path: fullPath }
} else if (type === 's3') {
const loc = `s3://${resolved.bucket || 'default'}/${resolved.prefix || ''}${backupId}`
return { type: 's3', location: loc }
} else if (type === 'smb') {
const loc = `\\\\${resolved.host || 'server'}\\${resolved.share || 'backups'}\\${backupId}`
return { type: 'smb', location: loc }
} else if (type === 'sftp') {
const loc = `sftp://${resolved.host || 'server'}:${resolved.port || 22}${resolved.path || '/backups'}/${backupId}`
return { type: 'sftp', location: loc }
} else if (type === 'pbs') {
const loc = `pbs://${resolved.server || 'server'}/${resolved.datastore || 'backup'}/${backupId}`
return { type: 'pbs', location: loc }
} else if (type === 'rclone') {
const loc = `rclone://${resolved.remote || 'remote'}:${resolved.path || ''}/${backupId}`
return { type: 'rclone', location: loc }
}
return { type, location: `${type}://${backupId}` }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Wrong location formatting 🐞 Bug ≡ Correctness

getStorageLocation fabricates adapter URLs (s3://…, sftp://…, pbs://…) that don’t correspond to how
several adapters actually store backups (restic repo + tags, PBS repo string), making the new
/api/backups/:id/location response misleading and sometimes syntactically invalid.
Agent Prompt
### Issue description
`PartialRestoreService.getStorageLocation()` currently constructs ad-hoc URIs (e.g., `s3://bucket/prefix<backupId>`, `sftp://host:portpath/...`, `pbs://server/datastore/...`) that do not match the real storage model for:
- Restic-backed adapters (s3/sftp/rclone/etc.): backups are snapshots inside a repo, tagged with `drk:<remotePath>`.
- PBS adapter: config is `repo` string (user@realm@host:port:datastore), not separate `server/datastore` fields.

### Issue Context
This endpoint is user-facing (banner + copy path). Incorrect values undermine the feature and can misdirect operators during recovery.

### Fix Focus Areas
- For local: keep returning absolute local path.
- For restic-backed storage: return the resolved restic repo location (as the adapter would compute it) and optionally include the logical tag/prefix (e.g., `drk:${backupId}/`).
- For PBS: return/echo the resolved `repo` (and maybe namespace/prefix semantics) rather than a fabricated `pbs://...` URL.
- Normalize joins (`prefix` separators, leading slashes) instead of string concatenation.

### Fix Focus Areas (code pointers)
- packages/backend/src/services/PartialRestoreService.ts[44-74]
- packages/backend/src/storage/adapters/ResticStorageAdapter.ts[7-16]
- packages/backend/src/storage/adapters/S3StorageAdapter.ts[19-23]
- packages/backend/src/storage/adapters/SFTPStorageAdapter.ts[22-35]
- packages/backend/src/connectors/PBSConnector.ts[11-20]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +98 to +107
private async isGzipped(filePath: string): Promise<boolean> {
try {
const fd = await fs.open(filePath, 'r')
const buf = Buffer.alloc(2)
await fs.read(fd, buf, 0, 2, 0)
await fs.close(fd)
return buf[0] === 0x1f && buf[1] === 0x8b
} catch {
return true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Gzip probe hides i/o 🐞 Bug ☼ Reliability

isGzipped() swallows all open/read errors by returning true and does not guarantee fd cleanup if
read fails after open, which can both misclassify archives and leak file descriptors under repeated
failures.
Agent Prompt
### Issue description
`isGzipped()` catches all errors and returns `true`, which can hide real filesystem issues and lead to confusing downstream tar failures. Additionally, if `fs.read` throws after `fs.open` succeeds, the fd won’t be closed.

### Issue Context
This runs on browse/extract paths and should be safe under partial I/O failures.

### Fix Focus Areas
- Ensure fd is closed via `finally` (or use `fs.promises.open()` FileHandle and `handle.close()` in finally).
- Do not treat generic I/O errors as “gzipped”; either rethrow (preferred) or return an explicit “unknown” and let callers try both tar modes.
- Optionally mirror `tarList()` fallback behavior in `extractFile()` when format detection is uncertain.

### Fix Focus Areas (code pointers)
- packages/backend/src/services/PartialRestoreService.ts[98-107]
- packages/backend/src/services/PartialRestoreService.ts[76-94]
- packages/backend/src/services/PartialRestoreService.ts[137-149]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +85 to +90
const copyPath = () => {
if (!locationInfo?.location) return
navigator.clipboard.writeText(locationInfo.location)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

4. Clipboard copy not awaited 🐞 Bug ☼ Reliability

copyPath() triggers clipboard writes without awaiting/catching and sets “Copied” before the write
succeeds; it also starts a timeout without cleanup, potentially leaving a pending timer after the
modal closes.
Agent Prompt
### Issue description
The UI marks the path as copied immediately and does not handle clipboard failures (permission / insecure context / API missing). It also creates a timeout that isn’t cleared if the component unmounts quickly.

### Issue Context
This is a UX/reliability issue: users can see “Copied” when the copy actually failed.

### Fix Focus Areas
- Make `copyPath` async and `await navigator.clipboard.writeText(...)`.
- Only set `copied=true` after success; on failure, show an error or keep `copied=false`.
- Store timeout id in a ref and clear it on unmount (cleanup effect).

### Fix Focus Areas (code pointers)
- packages/extension/src/components/PartialRestoreBrowser.tsx[85-90]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@GoZippy
GoZippy merged commit 60b1743 into main Jul 30, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants