Skip to content

Conversation

@haroondilshad
Copy link

@haroondilshad haroondilshad commented Sep 6, 2025

🐛 Bug Description

DevPod workspace uploads fail when extracting tar archives containing symlinks that already exist in the destination directory. This causes the following error:

symlink <target> <destination>: file exists

🔄 Bug Reproduction

Prerequisites

  • DevPod workspace with local folder source
  • Directory containing symlinks (e.g., link.md -> target.md)
  • Existing workspace that has been uploaded before

Steps to Reproduce

  1. Create a workspace with symlinks in the local folder:

    echo "content" > target.md
    ln -s target.md link.md
  2. Upload workspace to DevPod:

    devpod up my-workspace
  3. Make any change and rebuild the workspace:

    devpod up my-workspace --recreate
  4. Expected: Workspace rebuilds successfully

  5. Actual: Fails with symlink target.md link.md: file exists error

Root Cause

The tar extraction code in pkg/extract/extract.go calls os.Symlink() without checking if the target file already exists. When DevPod re-uploads the workspace, it tries to create symlinks that already exist from the previous upload, causing the extraction to fail.

✅ Solution

Changes Made

  1. Enhanced symlink extraction logic in pkg/extract/extract.go:

    • Check if file/symlink already exists at target location
    • If existing symlink points to the same target, preserve it (no-op)
    • If existing symlink has different target, remove and recreate
    • If regular file exists, remove and create symlink
    • Improve error messages with context
  2. Added comprehensive tests in pkg/extract/extract_test.go:

    • Test creating new symlinks
    • Test replacing existing symlinks with different targets
    • Test preserving existing symlinks with same targets
    • Test replacing regular files with symlinks
    • Test multiple symlinks in same archive
    • Test gzipped tar archives with symlinks

Code Changes

// Before (fails on existing files)
err := os.Symlink(header.Linkname, outFileName)
if err != nil {
    return false, err
}

// After (handles existing files intelligently)
if _, err := os.Lstat(outFileName); err == nil {
    if existingLink, err := os.Readlink(outFileName); err == nil {
        if existingLink == header.Linkname {
            return true, nil // Same symlink, no change needed
        }
    }
    // Remove existing file/symlink
    if err := os.Remove(outFileName); err != nil {
        return false, perrors.Wrapf(err, "remove existing file for symlink %s", outFileName)
    }
}

err := os.Symlink(header.Linkname, outFileName)
if err != nil {
    return false, perrors.Wrapf(err, "create symlink %s -> %s", outFileName, header.Linkname)
}

🧪 Testing

All tests pass:

$ go test ./pkg/extract -v
=== RUN   TestExtractSymlinkConflicts
=== RUN   TestExtractSymlinkConflicts/create_new_symlink
=== RUN   TestExtractSymlinkConflicts/replace_existing_symlink_different_target  
=== RUN   TestExtractSymlinkConflicts/preserve_existing_symlink_same_target
=== RUN   TestExtractSymlinkConflicts/replace_existing_regular_file
--- PASS: TestExtractSymlinkConflicts (0.00s)
=== RUN   TestExtractSymlinkMultipleConflicts
--- PASS: TestExtractSymlinkMultipleConflicts (0.00s)
=== RUN   TestExtractGzippedTarWithSymlinks  
--- PASS: TestExtractGzippedTarWithSymlinks (0.00s)
PASS

🎯 Impact

What This Fixes

  • ✅ DevPod workspace rebuilds no longer fail on symlink conflicts
  • ✅ No more manual intervention required to delete conflicting files
  • ✅ Symlinks are preserved when unchanged, replaced when different
  • ✅ Works with both regular and gzipped tar archives

Backward Compatibility

  • ✅ Fully backward compatible - only affects the failing case
  • ✅ No changes to existing successful extraction behavior
  • ✅ No breaking changes to API or interfaces

Performance

  • ✅ Minimal performance overhead (one extra file check per symlink)
  • ✅ No impact on extraction of regular files
  • ✅ Early return for unchanged symlinks avoids unnecessary work

📝 Related Issues

This fix addresses workspace upload failures experienced by users with symlinks in their projects, particularly common in documentation and configuration scenarios where multiple files reference a common target.

- Fix symlink extraction conflict when target file already exists
- Check if existing symlink points to same target before removing
- Add comprehensive tests for symlink conflict scenarios
- Resolves the 'file exists' error during workspace uploads

This fixes the issue where DevPod workspace uploads fail with:
'symlink GEMINI.md /path/to/AGENTS.md: file exists'

The fix ensures that:
1. If a symlink already exists with the same target, it's preserved
2. If a symlink exists with a different target, it's replaced
3. If a regular file exists, it's replaced with the symlink
4. New symlinks are created normally
- Replace specific GEMINI.md/AGENTS.md test names with generic target.txt/link1.txt/link2.txt
- Tests now focus on symlink behavior rather than specific file names
- Maintains comprehensive coverage of symlink conflict scenarios
@haroondilshad haroondilshad changed the title refactor: make symlink tests generic Fix: Handle existing symlinks during tar extraction Sep 6, 2025
@haroondilshad
Copy link
Author

@pascalbreuninger

@jrx-sjg
Copy link

jrx-sjg commented Oct 17, 2025

Hi, I'm facing exactly this problem. Can someone review this PR?

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