diff --git a/http.go b/http.go index 4b1e998..89bdba3 100644 --- a/http.go +++ b/http.go @@ -321,6 +321,23 @@ func (rt *Router) routeServerRenameFile(w http.ResponseWriter, r *http.Request, w.WriteHeader(http.StatusNoContent) } +func (rt *Router) routeServerCopyFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + s := rt.Servers.Get(ps.ByName("server")) + defer r.Body.Close() + + data := rt.ReaderToBytes(r.Body) + copyLocation, _ := jsonparser.GetString(data, "file_location") + + if err := s.Filesystem.Copy(copyLocation); err != nil { + zap.S().Errorw("error copying file for server", zap.String("server", s.Uuid), zap.Error(err)) + + http.Error(w, "an error occurred while copying the file", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + func (rt *Router) ReaderToBytes(r io.Reader) []byte { buf := bytes.Buffer{} buf.ReadFrom(r) @@ -339,6 +356,7 @@ func (rt *Router) ConfigureRouter() *httprouter.Router { router.GET("/api/servers/:server/files/read/*path", rt.AuthenticateToken("s:files", rt.AuthenticateServer(rt.routeServerFileRead))) router.GET("/api/servers/:server/files/list/*path", rt.AuthenticateToken("s:files", rt.AuthenticateServer(rt.routeServerFileList))) router.PUT("/api/servers/:server/files/rename", rt.AuthenticateToken("s:files", rt.AuthenticateServer(rt.routeServerRenameFile))) + router.POST("/api/servers/:server/files/copy", rt.AuthenticateToken("s:files", rt.AuthenticateServer(rt.routeServerCopyFile))) router.POST("/api/servers/:server/files/create-directory", rt.AuthenticateToken("s:files", rt.AuthenticateServer(rt.routeServerCreateDirectory))) router.POST("/api/servers/:server/power", rt.AuthenticateToken("s:power", rt.AuthenticateServer(rt.routeServerPower))) diff --git a/server/filesystem.go b/server/filesystem.go index 3a653b3..052f2ef 100644 --- a/server/filesystem.go +++ b/server/filesystem.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "github.com/gabriel-vasile/mimetype" "go.uber.org/zap" "io" @@ -12,6 +13,7 @@ import ( "path" "path/filepath" "sort" + "strconv" "strings" "sync" "time" @@ -287,6 +289,87 @@ func (fs *Filesystem) Rename(from string, to string) error { return os.Rename(cleanedFrom, cleanedTo) } +// Copies a given file to the same location and appends a suffix to the file to indicate that +// it has been copied. +// +// @todo need to get an exclusive lock on the file. +func (fs *Filesystem) Copy(p string) error { + cleaned, err := fs.SafePath(p) + if err != nil { + return err + } + + if s, err := os.Stat(cleaned); (err != nil && os.IsNotExist(err)) || s.IsDir() || !s.Mode().IsRegular() { + // For now I think I am okay just returning a nil response if the thing + // we're trying to copy doesn't exist. Probably will want to come back and + // re-evaluate if this is a smart decision (I'm guessing not). + return nil + } else if err != nil { + return err + } + + base := filepath.Base(cleaned) + relative := strings.TrimSuffix(strings.TrimPrefix(cleaned, fs.Path()), base) + extension := filepath.Ext(base) + name := strings.TrimSuffix(base, filepath.Ext(base)) + + // Begin looping up to 50 times to try and create a unique copy file name. This will take + // an input of "file.txt" and generate "file copy.txt". If that name is already taken, it will + // then try to write "file copy 2.txt" and so on, until reaching 50 loops. At that point we + // won't waste anymore time, just use the current timestamp and make that copy. + // + // Could probably make this more efficient by checking if there are any files matching the copy + // pattern, and trying to find the highest number and then incrementing it by one rather than + // looping endlessly. + var i int + copySuffix := " copy" + for i = 0; i < 51; i++ { + if i > 0 { + copySuffix = " copy " + strconv.Itoa(i) + } + + tryName := fmt.Sprintf("%s%s%s", name, copySuffix, extension) + tryLocation, err := fs.SafePath(path.Join(relative, tryName)) + if err != nil { + return err + } + + // If the file exists, continue to the next loop, otherwise we're good to start a copy. + if _, err := os.Stat(tryLocation); err != nil && !os.IsNotExist(err) { + return err + } else if os.IsNotExist(err) { + break + } + + if i == 50 { + copySuffix = "." + time.Now().Format(time.RFC3339) + } + } + + finalPath, err := fs.SafePath(path.Join(relative, fmt.Sprintf("%s%s%s", name, copySuffix, extension))) + if err != nil { + return err + } + + source, err := os.Open(cleaned) + if err != nil { + return err + } + defer source.Close() + + dest, err := os.Create(finalPath) + if err != nil { + return err + } + defer dest.Close() + + if _, err := io.Copy(dest, source); err != nil { + return err + } + + return nil +} + // Lists the contents of a given directory and returns stat information about each // file and folder within it. func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) {