| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "io" |
| 6 | "net/http" |
| 7 | "strconv" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | func (a *App) listBuckets(w http.ResponseWriter, r *http.Request) { |
| 12 | buckets, err := a.S3.ListBuckets(r.Context()) |
| 13 | data := bucketsPageData{Buckets: buckets} |
| 14 | if err != nil { |
| 15 | a.Log.Error("list buckets", "err", err) |
| 16 | data.Error = err.Error() |
| 17 | } |
| 18 | a.renderPage(w, "buckets.html", data) |
| 19 | } |
| 20 | |
| 21 | func (a *App) bucketRoot(w http.ResponseWriter, r *http.Request) { |
| 22 | bucket := r.PathValue("bucket") |
| 23 | http.Redirect(w, r, browseHref(bucket, ""), http.StatusSeeOther) |
| 24 | } |
| 25 | |
| 26 | func (a *App) browse(w http.ResponseWriter, r *http.Request) { |
| 27 | bucket := r.PathValue("bucket") |
| 28 | prefix := r.PathValue("prefix") |
| 29 | if prefix != "" && !strings.HasSuffix(prefix, "/") { |
| 30 | http.Redirect(w, r, browseHref(bucket, prefix+"/"), http.StatusSeeOther) |
| 31 | return |
| 32 | } |
| 33 | |
| 34 | folders, files, err := a.S3.List(r.Context(), bucket, prefix) |
| 35 | data := browsePageData{ |
| 36 | Bucket: bucket, |
| 37 | Prefix: prefix, |
| 38 | Crumbs: buildCrumbs(bucket, prefix), |
| 39 | Error: r.URL.Query().Get("error"), |
| 40 | Success: r.URL.Query().Get("success"), |
| 41 | MaxUploadMB: a.MaxUploadBytes >> 20, |
| 42 | } |
| 43 | if prefix != "" { |
| 44 | data.ParentHref = browseHref(bucket, parentPrefix(prefix)) |
| 45 | } |
| 46 | if err != nil { |
| 47 | a.Log.Error("list", "bucket", bucket, "prefix", prefix, "err", err) |
| 48 | if data.Error == "" { |
| 49 | data.Error = err.Error() |
| 50 | } |
| 51 | } |
| 52 | for _, f := range folders { |
| 53 | name := strings.TrimSuffix(strings.TrimPrefix(f, prefix), "/") |
| 54 | if name == "" { |
| 55 | continue |
| 56 | } |
| 57 | data.Folders = append(data.Folders, folderItem{ |
| 58 | Name: name, |
| 59 | Href: browseHref(bucket, f), |
| 60 | }) |
| 61 | } |
| 62 | for _, f := range files { |
| 63 | // Skip the zero-byte "folder marker" key that matches the prefix exactly. |
| 64 | if f.Name == "" { |
| 65 | continue |
| 66 | } |
| 67 | img := isImageName(f.Name) |
| 68 | item := fileItem{ |
| 69 | Name: f.Name, |
| 70 | Size: f.Size, |
| 71 | SizeHuman: humanSize(f.Size), |
| 72 | LastModified: f.LastModified, |
| 73 | DetailHref: objectHref(bucket, f.Key), |
| 74 | IsImage: img, |
| 75 | } |
| 76 | if img { |
| 77 | item.PreviewSrc = previewHref(bucket, f.Key) |
| 78 | } |
| 79 | data.Files = append(data.Files, item) |
| 80 | } |
| 81 | a.renderPage(w, "browse.html", data) |
| 82 | } |
| 83 | |
| 84 | func (a *App) detail(w http.ResponseWriter, r *http.Request) { |
| 85 | bucket := r.PathValue("bucket") |
| 86 | key := r.PathValue("key") |
| 87 | if key == "" { |
| 88 | http.NotFound(w, r) |
| 89 | return |
| 90 | } |
| 91 | meta, err := a.S3.Head(r.Context(), bucket, key) |
| 92 | if err != nil { |
| 93 | a.Log.Error("head", "bucket", bucket, "key", key, "err", err) |
| 94 | http.Error(w, "object not found", http.StatusNotFound) |
| 95 | return |
| 96 | } |
| 97 | parent := parentOfKey(key) |
| 98 | data := detailPageData{ |
| 99 | Bucket: bucket, |
| 100 | Key: key, |
| 101 | Name: nameOfKey(key), |
| 102 | ContentType: meta.ContentType, |
| 103 | Size: meta.Size, |
| 104 | SizeHuman: humanSize(meta.Size), |
| 105 | LastModified: meta.LastModified, |
| 106 | ETag: meta.ETag, |
| 107 | IsImage: isImageName(key) || strings.HasPrefix(meta.ContentType, "image/"), |
| 108 | PreviewSrc: previewHref(bucket, key), |
| 109 | ParentHref: browseHref(bucket, parent), |
| 110 | ParentPrefix: parent, |
| 111 | Crumbs: buildCrumbs(bucket, parent), |
| 112 | Error: r.URL.Query().Get("error"), |
| 113 | } |
| 114 | if u, perr := a.S3.PresignGet(r.Context(), bucket, key); perr == nil { |
| 115 | data.PresignedURL = u |
| 116 | } else { |
| 117 | a.Log.Warn("presign", "err", perr) |
| 118 | } |
| 119 | if u, ok := a.S3.PublicURL(bucket, key); ok { |
| 120 | data.PublicURL = u |
| 121 | data.HasPublic = true |
| 122 | } |
| 123 | a.renderPage(w, "detail.html", data) |
| 124 | } |
| 125 | |
| 126 | func (a *App) preview(w http.ResponseWriter, r *http.Request) { |
| 127 | bucket := r.PathValue("bucket") |
| 128 | key := r.PathValue("key") |
| 129 | if key == "" { |
| 130 | http.NotFound(w, r) |
| 131 | return |
| 132 | } |
| 133 | body, meta, err := a.S3.Get(r.Context(), bucket, key) |
| 134 | if err != nil { |
| 135 | a.Log.Error("get", "bucket", bucket, "key", key, "err", err) |
| 136 | http.Error(w, "object not found", http.StatusNotFound) |
| 137 | return |
| 138 | } |
| 139 | defer body.Close() |
| 140 | if meta.ContentType != "" { |
| 141 | w.Header().Set("Content-Type", meta.ContentType) |
| 142 | } |
| 143 | if meta.Size > 0 { |
| 144 | w.Header().Set("Content-Length", strconv.FormatInt(meta.Size, 10)) |
| 145 | } |
| 146 | if meta.ETag != "" { |
| 147 | w.Header().Set("ETag", `"`+meta.ETag+`"`) |
| 148 | } |
| 149 | w.Header().Set("Cache-Control", "private, max-age=60") |
| 150 | if _, err := io.Copy(w, body); err != nil && !errors.Is(err, http.ErrBodyReadAfterClose) { |
| 151 | a.Log.Warn("preview copy", "err", err) |
| 152 | } |
| 153 | } |