pico
created pr with
44.1
added 44.2
1: 89e0a31 ! 1: aa126f8 chore(prose): migrate images to pgs
changed status to
accepted
cmds
checkout latest patchset:
ssh pr.pico.sh print 44 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 44.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 44set PR to open (enables RSS notifications):
ssh pr.pico.sh pr open 44set PR to draft (stops RSS notifications):
ssh pr.pico.sh pr draft 44
Patchset
44.2
chore(prose): migrate images to pgs
Eric Bower
2025-01-18T14:27:41ZSemantic diff summary
6 added,
8 modified,
0 signature changed,
6 removed
across 5 analyzed files
(2 files skipped: unsupported file type)
+82
-0
cmd/scripts/prose-imgs-migrate/main.go
#
| ... | ... | @@ -0,0 +1,82 @@ | |
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "io" | |
| 6 | + | "log/slog" | |
| 7 | + | "path/filepath" | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | "github.com/picosh/pico/db" | |
| 11 | + | "github.com/picosh/pico/db/postgres" | |
| 12 | + | "github.com/picosh/pico/prose" | |
| 13 | + | "github.com/picosh/pico/shared" | |
| 14 | + | "github.com/picosh/pico/shared/storage" | |
| 15 | + | sst "github.com/picosh/pobj/storage" | |
| 16 | + | sendUtils "github.com/picosh/send/utils" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | func bail(err error) { | |
| 20 | + | if err != nil { | |
| 21 | + | panic(err) | |
| 22 | + | } | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | func upload(logger *slog.Logger, st storage.StorageServe, bucket sst.Bucket, fpath string, rdr io.Reader) error { | |
| 26 | + | toSite := filepath.Join("prose", fpath) | |
| 27 | + | logger.Info("uploading object", "bucket", bucket.Name, "object", toSite) | |
| 28 | + | buf := &bytes.Buffer{} | |
| 29 | + | size, err := io.Copy(buf, rdr) | |
| 30 | + | if err != nil { | |
| 31 | + | return err | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | _, _, err = st.PutObject(bucket, toSite, buf, &sendUtils.FileEntry{ | |
| 35 | + | Mtime: time.Now().Unix(), | |
| 36 | + | Size: size, | |
| 37 | + | }) | |
| 38 | + | return err | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | func images(logger *slog.Logger, st storage.StorageServe, bucket sst.Bucket, user *db.User) error { | |
| 42 | + | imgBucket, err := st.GetBucket(shared.GetImgsBucketName(user.ID)) | |
| 43 | + | if err != nil { | |
| 44 | + | logger.Info("user does not have an images dir, skipping") | |
| 45 | + | return nil | |
| 46 | + | } | |
| 47 | + | imgs, err := st.ListObjects(imgBucket, "/", false) | |
| 48 | + | if err != nil { | |
| 49 | + | return err | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | for _, inf := range imgs { | |
| 53 | + | rdr, _, err := st.GetObject(imgBucket, inf.Name()) | |
| 54 | + | if err != nil { | |
| 55 | + | return err | |
| 56 | + | } | |
| 57 | + | err = upload(logger, st, bucket, inf.Name(), rdr) | |
| 58 | + | if err != nil { | |
| 59 | + | return err | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | return nil | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func main() { | |
| 67 | + | cfg := prose.NewConfigSite() | |
| 68 | + | logger := cfg.Logger | |
| 69 | + | picoDb := postgres.NewDB(cfg.DbURL, logger) | |
| 70 | + | st, err := storage.NewStorageMinio(logger, cfg.MinioURL, cfg.MinioUser, cfg.MinioPass) | |
| 71 | + | bail(err) | |
| 72 | + | ||
| 73 | + | users, err := picoDb.FindUsers() | |
| 74 | + | bail(err) | |
| 75 | + | ||
| 76 | + | for _, user := range users { | |
| 77 | + | bucket, err := st.UpsertBucket(shared.GetAssetBucketName(user.ID)) | |
| 78 | + | bail(err) | |
| 79 | + | _, _ = picoDb.InsertProject(user.ID, "prose", "prose") | |
| 80 | + | bail(images(logger, st, bucket, user)) | |
| 81 | + | } | |
| 82 | + | } |
+8
-4
filehandlers/imgs/handler.go
#
| ... | ... | @@ -47,6 +47,10 @@ func NewUploadImgHandler(dbpool db.DB, cfg *shared.ConfigSite, storage storage.S | |
| 47 | 47 | } | |
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | + | func (h *UploadImgHandler) getObjectPath(fpath string) string { | |
| 51 | + | return filepath.Join("prose", fpath) | |
| 52 | + | } | |
| 53 | + | ||
| 50 | 54 | func (h *UploadImgHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.FileInfo, sendutils.ReaderAtCloser, error) { | |
| 51 | 55 | user, err := h.DBPool.FindUser(s.Permissions().Extensions["user_id"]) | |
| 52 | 56 | if err != nil { |
| ... | ... | @@ -71,12 +75,12 @@ func (h *UploadImgHandler) Read(s ssh.Session, entry *sendutils.FileEntry) (os.F | |
| 71 | 75 | FModTime: *post.UpdatedAt, | |
| 72 | 76 | } | |
| 73 | 77 | ||
| 74 | - | bucket, err := h.Storage.GetBucket(user.ID) | |
| 78 | + | bucket, err := h.Storage.GetBucket(shared.GetAssetBucketName(user.ID)) | |
| 75 | 79 | if err != nil { | |
| 76 | 80 | return nil, nil, err | |
| 77 | 81 | } | |
| 78 | 82 | ||
| 79 | - | contents, _, err := h.Storage.GetObject(bucket, post.Filename) | |
| 83 | + | contents, _, err := h.Storage.GetObject(bucket, h.getObjectPath(post.Filename)) | |
| 80 | 84 | if err != nil { | |
| 81 | 85 | return nil, nil, err | |
| 82 | 86 | } |
| ... | ... | @@ -218,13 +222,13 @@ func (h *UploadImgHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) err | |
| 218 | 222 | return fmt.Errorf("error for %s: %v", filename, err) | |
| 219 | 223 | } | |
| 220 | 224 | ||
| 221 | - | bucket, err := h.Storage.UpsertBucket(user.ID) | |
| 225 | + | bucket, err := h.Storage.UpsertBucket(shared.GetAssetBucketName(user.ID)) | |
| 222 | 226 | if err != nil { | |
| 223 | 227 | return err | |
| 224 | 228 | } | |
| 225 | 229 | ||
| 226 | 230 | logger.Info("deleting image") | |
| 227 | - | err = h.Storage.DeleteObject(bucket, filename) | |
| 231 | + | err = h.Storage.DeleteObject(bucket, h.getObjectPath(filename)) | |
| 228 | 232 | if err != nil { | |
| 229 | 233 | return err | |
| 230 | 234 | } |
+2
-24
filehandlers/imgs/img.go
#
| ... | ... | @@ -128,18 +128,6 @@ func (h *UploadImgHandler) writeImg(s ssh.Session, data *PostMetaData) error { | |
| 128 | 128 | logger.Error("post could not create", "err", err.Error()) | |
| 129 | 129 | return fmt.Errorf("error for %s: %v", data.Filename, err) | |
| 130 | 130 | } | |
| 131 | - | ||
| 132 | - | if len(data.Tags) > 0 { | |
| 133 | - | logger.Info( | |
| 134 | - | "found post tags, replacing with old tags", | |
| 135 | - | "tags", strings.Join(data.Tags, ","), | |
| 136 | - | ) | |
| 137 | - | err = h.DBPool.ReplaceTagsForPost(data.Tags, data.Post.ID) | |
| 138 | - | if err != nil { | |
| 139 | - | logger.Error("post could not replace tags", "err", err.Error()) | |
| 140 | - | return fmt.Errorf("error for %s: %v", data.Filename, err) | |
| 141 | - | } | |
| 142 | - | } | |
| 143 | 131 | } else { | |
| 144 | 132 | if data.Shasum == data.Cur.Shasum && modTime.Equal(*data.Cur.UpdatedAt) { | |
| 145 | 133 | logger.Info("image found, but image is identical, skipping") |
| ... | ... | @@ -167,16 +155,6 @@ func (h *UploadImgHandler) writeImg(s ssh.Session, data *PostMetaData) error { | |
| 167 | 155 | logger.Error("post could not update", "err", err.Error()) | |
| 168 | 156 | return fmt.Errorf("error for %s: %v", data.Filename, err) | |
| 169 | 157 | } | |
| 170 | - | ||
| 171 | - | logger.Info( | |
| 172 | - | "found post tags, replacing with old tags", | |
| 173 | - | "tags", strings.Join(data.Tags, ","), | |
| 174 | - | ) | |
| 175 | - | err = h.DBPool.ReplaceTagsForPost(data.Tags, data.Cur.ID) | |
| 176 | - | if err != nil { | |
| 177 | - | logger.Error("post could not replace tags", "err", err.Error()) | |
| 178 | - | return fmt.Errorf("error for %s: %v", data.Filename, err) | |
| 179 | - | } | |
| 180 | 158 | } | |
| 181 | 159 | ||
| 182 | 160 | return nil |
+0
-168
imgs/api.go
#
| ... | ... | @@ -1,168 +0,0 @@ | |
| 1 | - | package imgs | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "html/template" | |
| 6 | - | "net/http" | |
| 7 | - | "net/url" | |
| 8 | - | "path/filepath" | |
| 9 | - | ||
| 10 | - | "github.com/picosh/pico/db" | |
| 11 | - | "github.com/picosh/pico/pgs" | |
| 12 | - | "github.com/picosh/pico/shared" | |
| 13 | - | "github.com/picosh/pico/shared/storage" | |
| 14 | - | "github.com/picosh/utils" | |
| 15 | - | ) | |
| 16 | - | ||
| 17 | - | type PostPageData struct { | |
| 18 | - | ImgURL template.URL | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | type BlogPageData struct { | |
| 22 | - | Site *shared.SitePageData | |
| 23 | - | PageTitle string | |
| 24 | - | URL template.URL | |
| 25 | - | Username string | |
| 26 | - | Posts []template.URL | |
| 27 | - | } | |
| 28 | - | ||
| 29 | - | var Space = "imgs" | |
| 30 | - | ||
| 31 | - | func ImgsListHandler(w http.ResponseWriter, r *http.Request) { | |
| 32 | - | username := shared.GetUsernameFromRequest(r) | |
| 33 | - | dbpool := shared.GetDB(r) | |
| 34 | - | logger := shared.GetLogger(r) | |
| 35 | - | cfg := shared.GetCfg(r) | |
| 36 | - | ||
| 37 | - | user, err := dbpool.FindUserForName(username) | |
| 38 | - | if err != nil { | |
| 39 | - | logger.Info("blog not found", "username", username) | |
| 40 | - | http.Error(w, "blog not found", http.StatusNotFound) | |
| 41 | - | return | |
| 42 | - | } | |
| 43 | - | ||
| 44 | - | var posts []*db.Post | |
| 45 | - | pager := &db.Pager{Num: 1000, Page: 0} | |
| 46 | - | p, err := dbpool.FindPostsForUser(pager, user.ID, Space) | |
| 47 | - | posts = p.Data | |
| 48 | - | ||
| 49 | - | if err != nil { | |
| 50 | - | logger.Error(err.Error()) | |
| 51 | - | http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError) | |
| 52 | - | return | |
| 53 | - | } | |
| 54 | - | ||
| 55 | - | ts, err := shared.RenderTemplate(cfg, []string{ | |
| 56 | - | cfg.StaticPath("html/imgs.page.tmpl"), | |
| 57 | - | }) | |
| 58 | - | ||
| 59 | - | if err != nil { | |
| 60 | - | logger.Error(err.Error()) | |
| 61 | - | http.Error(w, err.Error(), http.StatusInternalServerError) | |
| 62 | - | return | |
| 63 | - | } | |
| 64 | - | ||
| 65 | - | curl := shared.CreateURLFromRequest(cfg, r) | |
| 66 | - | postCollection := make([]template.URL, 0, len(posts)) | |
| 67 | - | for _, post := range posts { | |
| 68 | - | url := cfg.ImgURL(curl, post.Username, post.Slug) | |
| 69 | - | postCollection = append(postCollection, template.URL(url)) | |
| 70 | - | } | |
| 71 | - | ||
| 72 | - | data := BlogPageData{ | |
| 73 | - | Site: cfg.GetSiteData(), | |
| 74 | - | PageTitle: fmt.Sprintf("%s imgs", username), | |
| 75 | - | URL: template.URL(cfg.FullBlogURL(curl, username)), | |
| 76 | - | Username: username, | |
| 77 | - | Posts: postCollection, | |
| 78 | - | } | |
| 79 | - | ||
| 80 | - | err = ts.Execute(w, data) | |
| 81 | - | if err != nil { | |
| 82 | - | logger.Error(err.Error()) | |
| 83 | - | http.Error(w, err.Error(), http.StatusInternalServerError) | |
| 84 | - | } | |
| 85 | - | } | |
| 86 | - | ||
| 87 | - | func anyPerm(proj *db.Project) bool { | |
| 88 | - | return true | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | func ImgRequest(w http.ResponseWriter, r *http.Request) { | |
| 92 | - | subdomain := shared.GetSubdomain(r) | |
| 93 | - | cfg := shared.GetCfg(r) | |
| 94 | - | st := shared.GetStorage(r) | |
| 95 | - | dbpool := shared.GetDB(r) | |
| 96 | - | logger := shared.GetLogger(r) | |
| 97 | - | username := shared.GetUsernameFromRequest(r) | |
| 98 | - | ||
| 99 | - | user, err := dbpool.FindUserForName(username) | |
| 100 | - | if err != nil { | |
| 101 | - | logger.Info("user not found", "user", username) | |
| 102 | - | http.Error(w, "user not found", http.StatusNotFound) | |
| 103 | - | return | |
| 104 | - | } | |
| 105 | - | ||
| 106 | - | var imgOpts string | |
| 107 | - | var slug string | |
| 108 | - | if !cfg.IsSubdomains() || subdomain == "" { | |
| 109 | - | slug, _ = url.PathUnescape(shared.GetField(r, 1)) | |
| 110 | - | imgOpts, _ = url.PathUnescape(shared.GetField(r, 2)) | |
| 111 | - | } else { | |
| 112 | - | slug, _ = url.PathUnescape(shared.GetField(r, 0)) | |
| 113 | - | imgOpts, _ = url.PathUnescape(shared.GetField(r, 1)) | |
| 114 | - | } | |
| 115 | - | ||
| 116 | - | opts, err := storage.UriToImgProcessOpts(imgOpts) | |
| 117 | - | if err != nil { | |
| 118 | - | errMsg := fmt.Sprintf("error processing img options: %s", err.Error()) | |
| 119 | - | logger.Info(errMsg) | |
| 120 | - | http.Error(w, errMsg, http.StatusUnprocessableEntity) | |
| 121 | - | return | |
| 122 | - | } | |
| 123 | - | ||
| 124 | - | // set default quality for web optimization | |
| 125 | - | if opts.Quality == 0 { | |
| 126 | - | opts.Quality = 80 | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | ext := filepath.Ext(slug) | |
| 130 | - | // set default format to be webp | |
| 131 | - | if opts.Ext == "" && ext == "" { | |
| 132 | - | opts.Ext = "webp" | |
| 133 | - | } | |
| 134 | - | ||
| 135 | - | // Files can contain periods. `filepath.Ext` is greedy and will clip the last period in the slug | |
| 136 | - | // and call that a file extension so we want to be explicit about what | |
| 137 | - | // file extensions we clip here | |
| 138 | - | for _, fext := range cfg.AllowedExt { | |
| 139 | - | if ext == fext { | |
| 140 | - | // users might add the file extension when requesting an image | |
| 141 | - | // but we want to remove that | |
| 142 | - | slug = utils.SanitizeFileExt(slug) | |
| 143 | - | break | |
| 144 | - | } | |
| 145 | - | } | |
| 146 | - | ||
| 147 | - | post, err := FindImgPost(r, user, slug) | |
| 148 | - | if err != nil { | |
| 149 | - | errMsg := fmt.Sprintf("image not found %s/%s", user.Name, slug) | |
| 150 | - | logger.Info(errMsg) | |
| 151 | - | http.Error(w, errMsg, http.StatusNotFound) | |
| 152 | - | return | |
| 153 | - | } | |
| 154 | - | ||
| 155 | - | fname := post.Filename | |
| 156 | - | router := pgs.NewWebRouter( | |
| 157 | - | cfg, | |
| 158 | - | logger, | |
| 159 | - | dbpool, | |
| 160 | - | st, | |
| 161 | - | ) | |
| 162 | - | router.ServeAsset(fname, opts, true, anyPerm, w, r) | |
| 163 | - | } | |
| 164 | - | ||
| 165 | - | func FindImgPost(r *http.Request, user *db.User, slug string) (*db.Post, error) { | |
| 166 | - | dbpool := shared.GetDB(r) | |
| 167 | - | return dbpool.FindPostWithSlug(slug, user.ID, Space) | |
| 168 | - | } |
+0
-0
imgs/public/.gitkeep
#
+21
-12
prose/api.go
#
| ... | ... | @@ -438,14 +438,6 @@ func postHandler(w http.ResponseWriter, r *http.Request) { | |
| 438 | 438 | WithStyles: withStyles, | |
| 439 | 439 | } | |
| 440 | 440 | } else { | |
| 441 | - | // TODO: HACK to support imgs slugs inside prose | |
| 442 | - | // We definitely want to kill this feature in time | |
| 443 | - | imgPost, err := imgs.FindImgPost(r, user, slug) | |
| 444 | - | if err == nil && imgPost != nil { | |
| 445 | - | imgs.ImgRequest(w, r) | |
| 446 | - | return | |
| 447 | - | } | |
| 448 | - | ||
| 449 | 441 | notFound, err := dbpool.FindPostWithFilename("_404.md", user.ID, cfg.Space) | |
| 450 | 442 | contents := template.HTML("Oops! we can't seem to find this post.") | |
| 451 | 443 | title := "Post not found" |
| ... | ... | @@ -859,6 +851,24 @@ func createMainRoutes(staticRoutes []shared.Route) []shared.Route { | |
| 859 | 851 | return routes | |
| 860 | 852 | } | |
| 861 | 853 | ||
| 854 | + | func imgRequest(w http.ResponseWriter, r *http.Request) { | |
| 855 | + | username := shared.GetUsernameFromRequest(r) | |
| 856 | + | destUrl, err := url.Parse(fmt.Sprintf("https://%s-prose.pgs.sh", username)) | |
| 857 | + | if err != nil { | |
| 858 | + | http.Error(w, "site not found", http.StatusNotFound) | |
| 859 | + | return | |
| 860 | + | } | |
| 861 | + | ||
| 862 | + | proxy := httputil.NewSingleHostReverseProxy(destUrl) | |
| 863 | + | oldDirector := proxy.Director | |
| 864 | + | proxy.Director = func(r *http.Request) { | |
| 865 | + | oldDirector(r) | |
| 866 | + | r.Host = destUrl.Host | |
| 867 | + | r.URL = destUrl | |
| 868 | + | } | |
| 869 | + | proxy.ServeHTTP(w, r) | |
| 870 | + | } | |
| 871 | + | ||
| 862 | 872 | func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route { | |
| 863 | 873 | routes := []shared.Route{ | |
| 864 | 874 | shared.NewRoute("GET", "/", blogHandler), |
| ... | ... | @@ -881,9 +891,8 @@ func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route { | |
| 881 | 891 | routes = append( | |
| 882 | 892 | routes, | |
| 883 | 893 | shared.NewRoute("GET", "/raw/(.+)", postRawHandler), | |
| 884 | - | shared.NewRoute("GET", "/([^/]+)/(.+)", imgs.ImgRequest), | |
| 885 | - | shared.NewRoute("GET", "/(.+.(?:jpg|jpeg|png|gif|webp|svg))$", imgs.ImgRequest), | |
| 886 | - | shared.NewRoute("GET", "/i", imgs.ImgsListHandler), | |
| 894 | + | shared.NewRoute("GET", "/([^/]+)/(.+)", imgRequest), | |
| 895 | + | shared.NewRoute("GET", "/(.+.(?:jpg|jpeg|png|gif|webp|svg))$", imgRequest), | |
| 887 | 896 | shared.NewRoute("GET", "/(.+)", postHandler), | |
| 888 | 897 | ) | |
| 889 | 898 |