11package build
22
33import (
4+ "errors"
45 "fmt"
6+ "io"
57 "os"
68 "path/filepath"
79 "pig/internal/config"
810 "pig/internal/utils"
11+ "syscall"
912
1013 "github.com/sirupsen/logrus"
1114)
1215
16+ var renamePath = os .Rename
17+
1318// Spec configuration for build environments
1419type specConfig struct {
1520 Type string // "rpm" or "deb"
@@ -102,7 +107,8 @@ func setupBuildDirs(spec *specConfig, force bool) error {
102107
103108// createSymlink creates a symbolic link: linkPath -> target
104109// In force mode, aggressively removes any existing file/dir/symlink at linkPath.
105- // In non-force mode, existing directories are preserved to avoid accidental data loss.
110+ // In non-force mode, existing directories are migrated into target first, then
111+ // replaced by symlink to preserve data while keeping link semantics.
106112func createSymlink (target , linkPath string , force bool ) error {
107113 if info , err := os .Lstat (linkPath ); err == nil {
108114 switch {
@@ -116,9 +122,14 @@ func createSymlink(target, linkPath string, force bool) error {
116122 }
117123 case info .IsDir ():
118124 if ! force {
119- // Keep existing directories in non-force mode.
120- logrus .Debugf ("keeping existing directory in non-force mode: %s" , linkPath )
121- return nil
125+ // Preserve existing content by moving it into target before relinking.
126+ if err := migrateDirIntoTarget (linkPath , target ); err != nil {
127+ return err
128+ }
129+ if err := os .Remove (linkPath ); err != nil && ! os .IsNotExist (err ) {
130+ return fmt .Errorf ("failed to remove existing directory after migration: %w" , err )
131+ }
132+ break
122133 }
123134 if err := os .RemoveAll (linkPath ); err != nil && ! os .IsNotExist (err ) {
124135 return fmt .Errorf ("failed to remove existing directory: %w" , err )
@@ -152,6 +163,133 @@ func createSymlink(target, linkPath string, force bool) error {
152163 return os .Symlink (target , linkPath )
153164}
154165
166+ func migrateDirIntoTarget (srcDir , targetDir string ) error {
167+ if srcDir == targetDir {
168+ return nil
169+ }
170+ if err := os .MkdirAll (targetDir , 0755 ); err != nil {
171+ return fmt .Errorf ("failed to create target directory %s: %w" , targetDir , err )
172+ }
173+ backupDir := filepath .Join (targetDir , ".migrated_from_" + filepath .Base (srcDir ))
174+
175+ entries , err := os .ReadDir (srcDir )
176+ if err != nil {
177+ return fmt .Errorf ("failed to read existing directory %s: %w" , srcDir , err )
178+ }
179+ for _ , entry := range entries {
180+ srcPath := filepath .Join (srcDir , entry .Name ())
181+ dstPath := filepath .Join (targetDir , entry .Name ())
182+
183+ if _ , err := os .Lstat (dstPath ); err == nil {
184+ // Keep target entry untouched; move source entry into backup to avoid data loss.
185+ if err := os .MkdirAll (backupDir , 0755 ); err != nil {
186+ return fmt .Errorf ("failed to create migration backup directory %s: %w" , backupDir , err )
187+ }
188+ backupPath , err := uniqueBackupPath (filepath .Join (backupDir , entry .Name ()))
189+ if err != nil {
190+ return fmt .Errorf ("failed to allocate migration backup path: %w" , err )
191+ }
192+ if err := os .Rename (srcPath , backupPath ); err != nil {
193+ return fmt .Errorf ("failed to backup conflicting entry %s to %s: %w" , srcPath , backupPath , err )
194+ }
195+ continue
196+ } else if ! os .IsNotExist (err ) {
197+ return fmt .Errorf ("failed to inspect migration target %s: %w" , dstPath , err )
198+ }
199+
200+ if err := movePath (srcPath , dstPath ); err != nil {
201+ return fmt .Errorf ("failed to migrate %s to %s: %w" , srcPath , dstPath , err )
202+ }
203+ }
204+ return nil
205+ }
206+
207+ func movePath (src , dst string ) error {
208+ if err := renamePath (src , dst ); err == nil {
209+ return nil
210+ } else if ! errors .Is (err , syscall .EXDEV ) {
211+ return err
212+ }
213+
214+ if err := copyPath (src , dst ); err != nil {
215+ return err
216+ }
217+ return os .RemoveAll (src )
218+ }
219+
220+ func copyPath (src , dst string ) error {
221+ info , err := os .Lstat (src )
222+ if err != nil {
223+ return err
224+ }
225+
226+ switch mode := info .Mode (); {
227+ case mode & os .ModeSymlink != 0 :
228+ target , err := os .Readlink (src )
229+ if err != nil {
230+ return err
231+ }
232+ if err := os .MkdirAll (filepath .Dir (dst ), 0755 ); err != nil {
233+ return err
234+ }
235+ return os .Symlink (target , dst )
236+ case info .IsDir ():
237+ if err := os .MkdirAll (dst , info .Mode ().Perm ()); err != nil {
238+ return err
239+ }
240+ entries , err := os .ReadDir (src )
241+ if err != nil {
242+ return err
243+ }
244+ for _ , entry := range entries {
245+ if err := copyPath (filepath .Join (src , entry .Name ()), filepath .Join (dst , entry .Name ())); err != nil {
246+ return err
247+ }
248+ }
249+ return nil
250+ case mode .IsRegular ():
251+ if err := os .MkdirAll (filepath .Dir (dst ), 0755 ); err != nil {
252+ return err
253+ }
254+ in , err := os .Open (src )
255+ if err != nil {
256+ return err
257+ }
258+ defer in .Close ()
259+
260+ out , err := os .OpenFile (dst , os .O_CREATE | os .O_WRONLY | os .O_TRUNC , info .Mode ().Perm ())
261+ if err != nil {
262+ return err
263+ }
264+ defer out .Close ()
265+
266+ if _ , err := io .Copy (out , in ); err != nil {
267+ return err
268+ }
269+ return out .Close ()
270+ default :
271+ return fmt .Errorf ("unsupported file mode for cross-device move: %s" , info .Mode ().String ())
272+ }
273+ }
274+
275+ func uniqueBackupPath (path string ) (string , error ) {
276+ if _ , err := os .Lstat (path ); os .IsNotExist (err ) {
277+ return path , nil
278+ } else if err != nil {
279+ return "" , err
280+ }
281+
282+ for i := 1 ; i < 1000 ; i ++ {
283+ candidate := fmt .Sprintf ("%s.%d" , path , i )
284+ if _ , err := os .Lstat (candidate ); os .IsNotExist (err ) {
285+ return candidate , nil
286+ } else if err != nil {
287+ return "" , err
288+ }
289+ }
290+ return "" , fmt .Errorf ("failed to allocate backup path for %s" , path )
291+ }
292+
155293// syncSpec: Download tarball and perform incremental sync via rsync
156294func syncSpec (spec * specConfig , force bool , mirror bool ) error {
157295 logrus .Info ("sync extension specs" )
0 commit comments