diff --git a/router/router_server_files.go b/router/router_server_files.go index 1b4237e..ffe5f35 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -339,10 +339,22 @@ func postServerDecompressFiles(c *gin.Context) { } if err := s.Filesystem.DecompressFile(data.RootPath, data.File); err != nil { - // Check if the file does not exist. - // NOTE: os.IsNotExist() does not work if the error is wrapped. if errors.Is(err, os.ErrNotExist) { - c.Status(http.StatusNotFound) + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ + "error": "The requested archive was not found.", + }) + return + } + + // If the file is busy for some reason just return a nicer error to the user since there is not + // much we specifically can do. They'll need to stop the running server process in order to overwrite + // a file like this. + if strings.Contains(err.Error(), "text file busy") { + s.Log().WithField("error", err).Warn("failed to decompress file due to busy text file") + + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "error": "One or more files this archive is attempting to overwrite are currently in use by another process. Please try again.", + }) return } diff --git a/server/filesystem.go b/server/filesystem.go index fcc1005..de85969 100644 --- a/server/filesystem.go +++ b/server/filesystem.go @@ -400,9 +400,10 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error { currentSize = stat.Size() } + o := &fileOpener{} // This will either create the file if it does not already exist, or open and // truncate the existing file. - file, err := os.OpenFile(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + file, err := o.open(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return errors.WithStack(err) } @@ -937,3 +938,29 @@ func (fs *Filesystem) handleWalkerError(err error, f os.FileInfo) error { return nil } + +type fileOpener struct { + busy uint +} + +// Attempts to open a given file up to "attempts" number of times, using a backoff. If the file +// cannot be opened because of a "text file busy" error, we will attempt until the number of attempts +// has been exhaused, at which point we will abort with an error. +func (fo *fileOpener) open(path string, flags int, perm os.FileMode) (*os.File, error) { + for { + f, err := os.OpenFile(path, flags, perm) + + // If there is an error because the text file is busy, go ahead and sleep for a few + // hundred milliseconds and then try again up to three times before just returning the + // error back to the caller. + // + // Based on code from: https://github.com/golang/go/issues/22220#issuecomment-336458122 + if err != nil && fo.busy < 3 && strings.Contains(err.Error(), "text file busy") { + time.Sleep(100 * time.Millisecond << fo.busy) + fo.busy++ + continue + } + + return f, err + } +} \ No newline at end of file