package main import ( "crypto/md5" "crypto/rand" "encoding/hex" "encoding/xml" "errors" "fmt" "io" "io/fs" "log" "math/big" "net/http" "os" "path/filepath" "strconv" "strings" "time" ) type server struct { rootDir string logger *log.Logger } func listBuckets(root *os.Root) (ListAllMyBucketsResult, error) { files, err := fs.ReadDir(root.FS(), ".") if err != nil { return ListAllMyBucketsResult{}, err } var buckets []Bucket for _, file := range files { fileInfo, err := file.Info() var lastModified string if err != nil { // Some error retrieving the dir info, setting LastModified to dummy value lastModified = "2000-01-01T00:00:00Z" } else { // NOTE: For actual birth time, see https://github.com/djherbis/times lastModified = fileInfo.ModTime().UTC().Format(time.RFC3339) } buckets = append(buckets, Bucket{ Name: file.Name(), CreationDate: lastModified, }) } result := ListAllMyBucketsResult{ Buckets: Buckets{Buckets: buckets}, } return result, nil } func listObjects(root *os.Root, bucketName string, prefix string) (ListBucketResult, error) { subpath, filter := filepath.Split(prefix) path := filepath.Join(bucketName, subpath) files, err := fs.ReadDir(root.FS(), path) if err != nil { if errors.Is(err, fs.ErrNotExist) { return ListBucketResult{KeyCount: 0, IsTruncated: false, Prefix: prefix}, nil } else { return ListBucketResult{}, err } } var objects []Object for _, file := range files { if strings.HasPrefix(file.Name(), filter) { fileInfo, err := file.Info() var lastModified string var objectSize int64 if err != nil { // Some error retrieving the file info, setting LastModified to dummy value lastModified = "2000-01-01T00:00:00Z" objectSize = 0 } else { lastModified = fileInfo.ModTime().UTC().Format(time.RFC3339) objectSize = fileInfo.Size() } key, _ := filepath.Rel(bucketName, filepath.Join(path, file.Name())) objects = append(objects, Object{ Key: key, LastModified: lastModified, Size: objectSize, }) } } result := ListBucketResult{ KeyCount: int64(len(objects)), IsTruncated: false, Prefix: prefix, Contents: objects, } return result, nil } func (s *server) cpGet(w http.ResponseWriter, r *http.Request) { root, err := os.OpenRoot(s.rootDir) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } defer root.Close() path := filepath.Join(r.PathValue("bucket"), r.PathValue("key")) fileInfo, err := root.Stat(path) if err != nil { if errors.Is(err, fs.ErrNotExist) { http.Error(w, fmt.Sprintf("Key %s does not exist", r.PathValue("key")), http.StatusNotFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } // NOTE: If the file is huge, we will open it even in HEAD requests, // which shouldn't be needed - but then using http.ServeContent gets trickier file, err := root.Open(path) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), file) } func (s *server) ls(w http.ResponseWriter, r *http.Request) { root, err := os.OpenRoot(s.rootDir) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } defer root.Close() bucketName := r.PathValue("bucket") if bucketName == "" { // List available buckets result, err := listBuckets(root) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } w.Header().Set("Content-Type", "application/xml") xml.NewEncoder(w).Encode(result) } else { // List objects // But first, check if bucket exists if _, err := root.Stat(bucketName); err != nil { if errors.Is(err, fs.ErrNotExist) { http.Error(w, "The specified bucket does not exist", http.StatusNotFound) return } } result, err := listObjects(root, bucketName, r.URL.Query().Get("prefix")) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/xml") xml.NewEncoder(w).Encode(result) } } func (s *server) mb(w http.ResponseWriter, r *http.Request) { root, err := os.OpenRoot(s.rootDir) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } defer root.Close() path := filepath.Join(r.PathValue("bucket")) bucketName := strings.Split(path, "/")[0] if err := root.Mkdir(bucketName, 0775); err != nil { if errors.Is(err, fs.ErrExist) { http.Error(w, "Your previous request to create the named bucket succeeded and you already own it.", http.StatusConflict) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } w.WriteHeader(http.StatusOK) } func (s *server) rb(w http.ResponseWriter, r *http.Request) { root, err := os.OpenRoot(s.rootDir) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } defer root.Close() path := filepath.Join(r.PathValue("bucket")) bucketName := strings.Split(path, "/")[0] files, err := fs.ReadDir(root.FS(), bucketName) if err != nil { if errors.Is(err, fs.ErrNotExist) { http.Error(w, "The specified bucket does not exist", http.StatusConflict) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } if len(files) > 0 { http.Error(w, "The bucket you tried to delete is not empty", http.StatusConflict) return } if err := root.Remove(bucketName); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } func (s *server) cpPut(w http.ResponseWriter, r *http.Request) { root, err := os.OpenRoot(s.rootDir) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer root.Close() path := filepath.Join(r.PathValue("bucket"), r.PathValue("key")) // Prevents `cp` from accidentally creating a new bucket if it doesn't exist bucketName := strings.Split(path, "/")[0] if _, err := root.Stat(bucketName); err != nil { if errors.Is(err, fs.ErrNotExist) { http.Error(w, "The specified bucket does not exist", http.StatusNotFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } randomInt, _ := rand.Int(rand.Reader, big.NewInt(1000)) tempName := fmt.Sprintf(".tmp.%d.%s", randomInt, filepath.Base(path)) tempPath := filepath.Join(bucketName, tempName) tempFile, err := root.Create(tempPath) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer root.Remove(tempPath) defer tempFile.Close() hasher := md5.New() teeReader := io.TeeReader(r.Body, hasher) bytesWritten, err := io.Copy(tempFile, teeReader) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // We ignore errors here, if anything, the program will break when renaming tempFile.Sync() tempFile.Close() expectedContentLength := r.Header.Get("Content-Length") actualContentLength := strconv.FormatInt(bytesWritten, 10) if expectedContentLength != "" && expectedContentLength != actualContentLength { http.Error( w, fmt.Sprintf( "Expected Content-Length was %s, gor %s", expectedContentLength, actualContentLength, ), http.StatusInternalServerError, ) } // Safely create nested directories if needed // TODO: Remove dangling paths if anything fails if err := root.MkdirAll(filepath.Dir(path), 0775); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Move temporary file to final destination if err := root.Rename(tempPath, path); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } etag := fmt.Sprintf("\"%s\"", hex.EncodeToString(hasher.Sum(nil))) w.Header().Set("ETag", etag) w.WriteHeader(http.StatusOK) } func (s *server) rm(w http.ResponseWriter, r *http.Request) { root, err := os.OpenRoot(s.rootDir) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } defer root.Close() // TODO: Dangling empty directory path := filepath.Join(r.PathValue("bucket"), r.PathValue("key")) if err := root.Remove(path); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) }