Simple S3-like server for development purposes, written in Go
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Complete ls responses

+117 -17
+36
responses.go
··· 1 + package main 2 + 3 + import "encoding/xml" 4 + 5 + // https://docs.aws.amazon.com/AmazonS3/latest/API/API_Bucket.html 6 + type Bucket struct { 7 + Name string `xml:"Name"` 8 + CreationDate string `xml:"CreationDate"` 9 + } 10 + 11 + // https://docs.aws.amazon.com/AmazonS3/latest/API/API_Object.html 12 + type Object struct { 13 + Key string `xml:"Key"` 14 + LastModified string `xml:"LastModified"` 15 + // ETag 16 + Size int64 `xml:"Size"` 17 + // StorageClass 18 + } 19 + 20 + type Buckets struct { 21 + Buckets []Bucket `xml:"Bucket"` 22 + } 23 + 24 + // https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html 25 + type ListAllMyBucketsResult struct { 26 + XMLName xml.Name `xml:"ListAllMyBucketsResult"` 27 + Buckets Buckets `xml:"Buckets"` 28 + } 29 + 30 + // https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html 31 + type ListBucketResult struct { 32 + XMLName xml.Name `xml:"ListBucketResult"` 33 + Name string `xml:"Name"` 34 + Prefix string `xml:"Prefix"` 35 + Contents []Object `xml:"Contents"` 36 + }
+81 -17
server.go
··· 1 1 package main 2 2 3 3 import ( 4 + "encoding/xml" 4 5 "errors" 5 6 "io" 6 7 "io/fs" ··· 9 10 "os" 10 11 "path/filepath" 11 12 "strings" 13 + "time" 12 14 ) 13 15 14 16 type server struct { ··· 16 18 logger *log.Logger 17 19 } 18 20 21 + func listBuckets(root *os.Root) (ListAllMyBucketsResult, error) { 22 + files, err := fs.ReadDir(root.FS(), ".") 23 + if err != nil { 24 + return ListAllMyBucketsResult{}, err 25 + } 26 + 27 + var buckets []Bucket 28 + for _, file := range files { 29 + fileInfo, err := file.Info() 30 + var lastModified string 31 + if err != nil { 32 + // Some error retrieving the dir info, setting LastModified to dummy value 33 + lastModified = "2000-01-01T00:00:00Z" 34 + } else { 35 + // NOTE: For actual birth time, see https://github.com/djherbis/times 36 + lastModified = fileInfo.ModTime().Format(time.RFC3339) 37 + } 38 + buckets = append(buckets, Bucket{ 39 + Name: file.Name(), 40 + CreationDate: lastModified, 41 + }) 42 + } 43 + result := ListAllMyBucketsResult{ 44 + Buckets: Buckets{Buckets: buckets}, 45 + } 46 + return result, nil 47 + } 48 + 49 + func listObjects(root *os.Root, bucketName string, prefix string) (ListBucketResult, error) { 50 + subpath, filter := filepath.Split(prefix) 51 + path := filepath.Join(bucketName, subpath) 52 + files, err := fs.ReadDir(root.FS(), path) 53 + if err != nil { 54 + return ListBucketResult{}, err 55 + } 56 + 57 + var objects []Object 58 + for _, file := range files { 59 + if strings.HasPrefix(file.Name(), filter) { 60 + fileInfo, err := file.Info() 61 + var lastModified string 62 + var objectSize int64 63 + if err != nil { 64 + // Some error retrieving the file info, setting LastModified to dummy value 65 + lastModified = "2000-01-01T00:00:00Z" 66 + objectSize = 0 67 + } else { 68 + lastModified = fileInfo.ModTime().UTC().Format(time.RFC3339) 69 + objectSize = fileInfo.Size() 70 + } 71 + objects = append(objects, Object{ 72 + Key: file.Name(), 73 + LastModified: lastModified, 74 + Size: objectSize, 75 + }) 76 + } 77 + } 78 + result := ListBucketResult{ 79 + Contents: objects, 80 + } 81 + return result, nil 82 + } 83 + 19 84 func (s *server) ls(w http.ResponseWriter, r *http.Request) { 20 85 root, err := os.OpenRoot(s.rootDir) 21 86 if err != nil { ··· 26 91 bucketName := r.PathValue("bucket") 27 92 if bucketName == "" { 28 93 // List available buckets 29 - files, err := fs.ReadDir(root.FS(), ".") 94 + result, err := listBuckets(root) 30 95 if err != nil { 31 96 http.Error(w, err.Error(), http.StatusInternalServerError) 32 - return 33 97 } 34 - s.logger.Printf("Buckets: %s", files) 98 + 99 + w.Header().Set("Content-Type", "application/xml") 100 + xml.NewEncoder(w).Encode(result) 35 101 } else { 36 - prefix := r.URL.Query().Get("prefix") 102 + // List objects 103 + // But first, check if bucket exists 104 + if _, err := root.Stat(bucketName); err != nil { 105 + if errors.Is(err, fs.ErrNotExist) { 106 + http.Error(w, "The specified bucket does not exist", http.StatusNotFound) 107 + return 108 + } 109 + } 37 110 38 - subpath, filter := filepath.Split(prefix) 39 - path := filepath.Join(bucketName, subpath) 40 - s.logger.Printf("Path %s + filter '%s' (original prefix %s)", path, filter, prefix) 41 - files, err := fs.ReadDir(root.FS(), path) 111 + result, err := listObjects(root, bucketName, r.URL.Query().Get("prefix")) 42 112 if err != nil { 43 113 http.Error(w, err.Error(), http.StatusInternalServerError) 44 - return 45 - } 46 - for _, file := range files { 47 - if strings.HasPrefix(file.Name(), filter) { 48 - s.logger.Printf("File %s", file) 49 - } 50 114 } 51 - } 52 115 53 - // TODO: Probably requires actual XML 54 - w.WriteHeader(http.StatusNotImplemented) 116 + w.Header().Set("Content-Type", "application/xml") 117 + xml.NewEncoder(w).Encode(result) 118 + } 55 119 } 56 120 57 121 func (s *server) mb(w http.ResponseWriter, r *http.Request) {