@@ -172,7 +172,11 @@ func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []str
172172 baseName := strings .TrimSuffix (filepath .Base (toInstall ), filepath .Ext (toInstall ))
173173 assetFile = "." + baseName + ".app"
174174 } else {
175- assetFile = "." + filepath .Base (toInstall ) + ".asset"
175+ if systemdquadlet .IsExtSupported (toInstall ) {
176+ assetFile = "." + filepath .Base (toInstall ) + ".app"
177+ } else {
178+ assetFile = "." + filepath .Base (toInstall ) + ".asset"
179+ }
176180 }
177181 validateQuadletFile = true
178182 }
@@ -335,63 +339,102 @@ func (ic *ContainerEngine) installQuadlet(_ context.Context, path, destName, ins
335339 return "" , fmt .Errorf ("%q is not a supported Quadlet file type" , filepath .Ext (finalPath ))
336340 }
337341
338- osFlags := os .O_CREATE | os .O_WRONLY
342+ var destFile * os.File
343+ var tempPath string
339344
340345 if ! replace {
341- osFlags |= os .O_EXCL
346+ var err error
347+ // O_EXCL ensures we fail if the file already exists (avoids TOCTOU race)
348+ destFile , err = os .OpenFile (finalPath , os .O_CREATE | os .O_WRONLY | os .O_EXCL , 0o644 )
349+ if err != nil {
350+ if errors .Is (err , fs .ErrExist ) {
351+ return "" , fmt .Errorf ("a Quadlet with name %s already exists, refusing to overwrite" , filepath .Base (finalPath ))
352+ }
353+ return "" , fmt .Errorf ("unable to open file %s: %w" , finalPath , err )
354+ }
355+ } else {
356+ var err error
357+ destFile , err = os .CreateTemp (filepath .Dir (finalPath ), ".quadlet-install-*" )
358+ if err != nil {
359+ return "" , fmt .Errorf ("unable to create temp file: %w" , err )
360+ }
361+ tempPath = destFile .Name ()
342362 }
343363
344- file , err := os .OpenFile (finalPath , osFlags , 0o644 )
345- if err != nil {
346- if errors .Is (err , fs .ErrExist ) && ! replace {
347- return "" , fmt .Errorf ("a Quadlet with name %s already exists, refusing to overwrite" , filepath .Base (finalPath ))
364+ defer func () {
365+ if destFile != nil {
366+ destFile .Close ()
348367 }
349- return "" , fmt .Errorf ("unable to open file %s: %w" , filepath .Base (finalPath ), err )
350- }
351- defer file .Close ()
368+ if tempPath != "" {
369+ os .Remove (tempPath )
370+ }
371+ }()
352372
353- // Move the file in
354373 srcFile , err := os .Open (path )
355374 if err != nil {
356375 return "" , fmt .Errorf ("unable to open file: %w" , err )
357376 }
358377 defer srcFile .Close ()
359378
360- err = fileutils .ReflinkOrCopy (srcFile , file )
379+ err = fileutils .ReflinkOrCopy (srcFile , destFile )
361380 if err != nil {
362381 return "" , fmt .Errorf ("unable to copy file from %s to %s: %w" , path , finalPath , err )
363382 }
364383
365- // When we install files using this function, caller of this function can turn off `validateQuadletFile`
366- // when they are installing `non-quadlet` files.
384+ // Close before rename to flush writes; nil out to prevent double-close in defer
385+ if err := destFile .Close (); err != nil {
386+ return "" , fmt .Errorf ("unable to close file: %w" , err )
387+ }
388+ destFile = nil
389+
390+ if tempPath != "" {
391+ if err := os .Chmod (tempPath , 0o644 ); err != nil {
392+ return "" , fmt .Errorf ("unable to set permissions on temp file: %w" , err )
393+ }
394+
395+ if err := os .Rename (tempPath , finalPath ); err != nil {
396+ return "" , fmt .Errorf ("unable to rename temp file to %s: %w" , finalPath , err )
397+ }
398+ tempPath = ""
399+ }
400+
367401 if ! isQuadletFile {
368- err := appendStringToFile (filepath .Join (installDir , assetFile ), filepath .Base (filepath .Clean (path )))
402+ err := appendLineToFile (filepath .Join (installDir , assetFile ), filepath .Base (filepath .Clean (path )))
369403 if err != nil {
370404 return "" , fmt .Errorf ("error while writing non-quadlet filename: %w" , err )
371405 }
372406 } else if strings .HasSuffix (assetFile , ".app" ) {
373- // For quadlet files that are part of an application (indicated by .app extension),
374- // also write the quadlet filename to the .app file for proper application tracking
375407 quadletName := filepath .Base (finalPath )
376- err := appendStringToFile (filepath .Join (installDir , assetFile ), quadletName )
408+ err := appendLineToFile (filepath .Join (installDir , assetFile ), quadletName )
377409 if err != nil {
378410 return "" , fmt .Errorf ("error while writing quadlet filename to app file: %w" , err )
379411 }
380412 }
381413 return finalPath , nil
382414}
383415
384- // appendStringToFile appends the given text to the specified file.
385- // If the file does not exist, it will be created with 0644 permissions.
386- func appendStringToFile (filePath , text string ) error {
387- f , err := os .OpenFile (filePath , os .O_APPEND | os .O_CREATE | os .O_WRONLY , 0o644 )
416+ // appendLineToFile appends the given text as a line to the specified file,
417+ // ensuring it does not already exist (idempotency).
418+ func appendLineToFile (path , text string ) error {
419+ content , err := os .ReadFile (path )
420+ if err == nil {
421+ for _ , line := range strings .Split (string (content ), "\n " ) {
422+ if line == text {
423+ return nil // Already exists, do nothing
424+ }
425+ }
426+ }
427+
428+ f , err := os .OpenFile (path , os .O_APPEND | os .O_CREATE | os .O_WRONLY , 0o644 )
388429 if err != nil {
389430 return err
390431 }
391432 defer f .Close ()
392433
393- _ , err = f .WriteString (text + "\n " )
394- return err
434+ if _ , err := f .WriteString (text + "\n " ); err != nil {
435+ return err
436+ }
437+ return nil
395438}
396439
397440// quadletSection represents a single quadlet extracted from a multi-quadlet file
0 commit comments