Compare commits
No commits in common. "5559e0134382a141f5edabdacf1dc81f12b55c27" and "68f3215df6e2e4f345076dd5b20b9bf5867353cf" have entirely different histories.
5559e01343
...
68f3215df6
@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
cfgpkg "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
|
cfgpkg "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@ -100,7 +99,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
assetStore := asset.NewStore(postDB)
|
assetStore := post.NewAssetStore(postDB)
|
||||||
|
|
||||||
setAsset := func(assetID, assetPath string) error {
|
setAsset := func(assetID, assetPath string) error {
|
||||||
assetFullPath := filepath.Join(testDataDir, assetPath)
|
assetFullPath := filepath.Join(testDataDir, assetPath)
|
||||||
|
@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/http"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/http"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset"
|
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/pow"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/pow"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
||||||
@ -101,13 +100,7 @@ func main() {
|
|||||||
defer postSQLDB.Close()
|
defer postSQLDB.Close()
|
||||||
|
|
||||||
postStore := post.NewStore(postSQLDB)
|
postStore := post.NewStore(postSQLDB)
|
||||||
|
postAssetStore := post.NewAssetStore(postSQLDB)
|
||||||
postAssetStore := asset.NewStore(postSQLDB)
|
|
||||||
|
|
||||||
postAssetLoader := asset.NewStoreLoader(postAssetStore)
|
|
||||||
postAssetLoader = asset.NewArchiveLoader(postAssetLoader)
|
|
||||||
postAssetLoader = asset.NewImageLoader(postAssetLoader)
|
|
||||||
|
|
||||||
postDraftStore := post.NewDraftStore(postSQLDB)
|
postDraftStore := post.NewDraftStore(postSQLDB)
|
||||||
|
|
||||||
cache := cache.New(5000)
|
cache := cache.New(5000)
|
||||||
@ -117,7 +110,6 @@ func main() {
|
|||||||
httpParams.PowManager = powMgr
|
httpParams.PowManager = powMgr
|
||||||
httpParams.PostStore = postStore
|
httpParams.PostStore = postStore
|
||||||
httpParams.PostAssetStore = postAssetStore
|
httpParams.PostAssetStore = postAssetStore
|
||||||
httpParams.PostAssetLoader = postAssetLoader
|
|
||||||
httpParams.PostDraftStore = postDraftStore
|
httpParams.PostDraftStore = postDraftStore
|
||||||
httpParams.MailingList = ml
|
httpParams.MailingList = ml
|
||||||
httpParams.GeminiPublicURL = gmiParams.PublicURL
|
httpParams.GeminiPublicURL = gmiParams.PublicURL
|
||||||
@ -139,7 +131,7 @@ func main() {
|
|||||||
gmiParams.Logger = logger.WithNamespace("gmi")
|
gmiParams.Logger = logger.WithNamespace("gmi")
|
||||||
gmiParams.Cache = cache
|
gmiParams.Cache = cache
|
||||||
gmiParams.PostStore = postStore
|
gmiParams.PostStore = postStore
|
||||||
gmiParams.PostAssetLoader = postAssetLoader
|
gmiParams.PostAssetStore = postAssetStore
|
||||||
gmiParams.HTTPPublicURL = httpParams.PublicURL
|
gmiParams.HTTPPublicURL = httpParams.PublicURL
|
||||||
|
|
||||||
logger.Info(ctx, "starting gmi api")
|
logger.Info(ctx, "starting gmi api")
|
||||||
|
@ -2,16 +2,13 @@
|
|||||||
package gmi
|
package gmi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"mime"
|
"mime"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.sr.ht/~adnano/go-gemini"
|
"git.sr.ht/~adnano/go-gemini"
|
||||||
@ -19,7 +16,6 @@ import (
|
|||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
||||||
)
|
)
|
||||||
@ -30,8 +26,8 @@ type Params struct {
|
|||||||
Logger *mlog.Logger
|
Logger *mlog.Logger
|
||||||
Cache cache.Cache
|
Cache cache.Cache
|
||||||
|
|
||||||
PostStore post.Store
|
PostStore post.Store
|
||||||
PostAssetLoader asset.Loader
|
PostAssetStore post.AssetStore
|
||||||
|
|
||||||
PublicURL *url.URL
|
PublicURL *url.URL
|
||||||
ListenAddr string
|
ListenAddr string
|
||||||
@ -211,20 +207,18 @@ func (a *api) assetsMiddleware() gemini.Handler {
|
|||||||
r *gemini.Request,
|
r *gemini.Request,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
path := strings.TrimPrefix(r.URL.Path, "/assets/")
|
id := path.Base(r.URL.Path)
|
||||||
mimeType := mime.TypeByExtension(filepath.Ext(path))
|
mimeType := mime.TypeByExtension(path.Ext(id))
|
||||||
|
|
||||||
ctx = mctx.Annotate(ctx, "assetPath", path, "mimeType", mimeType)
|
ctx = mctx.Annotate(ctx, "assetID", id, "mimeType", mimeType)
|
||||||
|
|
||||||
if mimeType != "" {
|
if mimeType != "" {
|
||||||
rw.SetMediaType(mimeType)
|
rw.SetMediaType(mimeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
err := a.params.PostAssetStore.Get(id, rw)
|
||||||
|
|
||||||
err := a.params.PostAssetLoader.Load(path, buf, asset.LoadOpts{})
|
if errors.Is(err, post.ErrAssetNotFound) {
|
||||||
|
|
||||||
if errors.Is(err, asset.ErrNotFound) {
|
|
||||||
rw.WriteHeader(gemini.StatusNotFound, "Asset not found, sorry!")
|
rw.WriteHeader(gemini.StatusNotFound, "Asset not found, sorry!")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -233,12 +227,6 @@ func (a *api) assetsMiddleware() gemini.Handler {
|
|||||||
rw.WriteHeader(gemini.StatusTemporaryFailure, err.Error())
|
rw.WriteHeader(gemini.StatusTemporaryFailure, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := io.Copy(rw, buf); err != nil {
|
|
||||||
a.params.Logger.Error(ctx, "error copying asset", err)
|
|
||||||
rw.WriteHeader(gemini.StatusTemporaryFailure, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,17 +2,67 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
||||||
|
"github.com/omeid/go-tarfs"
|
||||||
|
"golang.org/x/image/draw"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func isImgResizable(path string) bool {
|
||||||
|
switch strings.ToLower(filepath.Ext(path)) {
|
||||||
|
case ".jpg", ".jpeg", ".png":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resizeImage(out io.Writer, in io.Reader, maxWidth float64) error {
|
||||||
|
|
||||||
|
img, format, err := image.Decode(in)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decoding image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
imgRect := img.Bounds()
|
||||||
|
imgW, imgH := float64(imgRect.Dx()), float64(imgRect.Dy())
|
||||||
|
|
||||||
|
if imgW > maxWidth {
|
||||||
|
|
||||||
|
newH := imgH * maxWidth / imgW
|
||||||
|
newImg := image.NewRGBA(image.Rect(0, 0, int(maxWidth), int(newH)))
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
draw.BiLinear.Scale(
|
||||||
|
newImg, newImg.Bounds(), img, img.Bounds(), draw.Over, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
img = newImg
|
||||||
|
}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case "jpeg":
|
||||||
|
return jpeg.Encode(out, img, nil)
|
||||||
|
case "png":
|
||||||
|
return png.Encode(out, img)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown image format %q", format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *api) managePostAssetsHandler() http.Handler {
|
func (a *api) managePostAssetsHandler() http.Handler {
|
||||||
|
|
||||||
tpl := a.mustParseBasedTpl("post-assets-manage.html")
|
tpl := a.mustParseBasedTpl("post-assets-manage.html")
|
||||||
@ -38,47 +88,173 @@ func (a *api) managePostAssetsHandler() http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type postAssetArchiveInfo struct {
|
||||||
|
path string
|
||||||
|
id string
|
||||||
|
subPath string
|
||||||
|
isGzipped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPostAssetArchiveInfo(path string) (postAssetArchiveInfo, bool) {
|
||||||
|
|
||||||
|
var info postAssetArchiveInfo
|
||||||
|
|
||||||
|
info.path = strings.TrimPrefix(path, "/")
|
||||||
|
|
||||||
|
info.id, info.subPath, _ = strings.Cut(info.path, "/")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
|
||||||
|
case strings.HasSuffix(info.id, ".tar.gz"),
|
||||||
|
strings.HasSuffix(info.id, ".tgz"):
|
||||||
|
info.isGzipped = true
|
||||||
|
|
||||||
|
case strings.HasSuffix(info.id, ".tar"):
|
||||||
|
// ok
|
||||||
|
|
||||||
|
default:
|
||||||
|
// unsupported
|
||||||
|
return postAssetArchiveInfo{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *api) writePostAsset(
|
||||||
|
rw http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
path string,
|
||||||
|
from io.ReadSeeker,
|
||||||
|
) {
|
||||||
|
|
||||||
|
maxWidth, err := apiutil.StrToInt(r.FormValue("w"), 0)
|
||||||
|
if err != nil {
|
||||||
|
apiutil.BadRequest(rw, r, fmt.Errorf("invalid w parameter: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxWidth == 0 {
|
||||||
|
http.ServeContent(rw, r, path, time.Time{}, from)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isImgResizable(path) {
|
||||||
|
apiutil.BadRequest(rw, r, fmt.Errorf("cannot resize asset %q", path))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resizedBuf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
if err := resizeImage(resizedBuf, from, float64(maxWidth)); err != nil {
|
||||||
|
apiutil.InternalServerError(
|
||||||
|
rw, r,
|
||||||
|
fmt.Errorf(
|
||||||
|
"resizing image %q to size %d: %w",
|
||||||
|
path, maxWidth, err,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeContent(
|
||||||
|
rw, r, path, time.Time{}, bytes.NewReader(resizedBuf.Bytes()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *api) handleGetPostAssetArchive(
|
||||||
|
rw http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
info postAssetArchiveInfo,
|
||||||
|
) {
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
err := a.params.PostAssetStore.Get(info.id, buf)
|
||||||
|
|
||||||
|
if errors.Is(err, post.ErrAssetNotFound) {
|
||||||
|
http.Error(rw, "asset not found", 404)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
apiutil.InternalServerError(
|
||||||
|
rw, r,
|
||||||
|
fmt.Errorf("fetching archive asset with id %q: %w", info.id, err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var from io.Reader = buf
|
||||||
|
|
||||||
|
if info.isGzipped {
|
||||||
|
|
||||||
|
if from, err = gzip.NewReader(from); err != nil {
|
||||||
|
apiutil.InternalServerError(
|
||||||
|
rw, r,
|
||||||
|
fmt.Errorf("decompressing archive asset with id %q: %w", info.id, err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tarFS, err := tarfs.New(from)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
apiutil.InternalServerError(
|
||||||
|
rw, r,
|
||||||
|
fmt.Errorf("reading archive asset with id %q as fs: %w", info.id, err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := tarFS.Open(info.subPath)
|
||||||
|
|
||||||
|
if errors.Is(err, fs.ErrExist) {
|
||||||
|
http.Error(rw, "Asset not found", 404)
|
||||||
|
return
|
||||||
|
|
||||||
|
} else if err != nil {
|
||||||
|
|
||||||
|
apiutil.InternalServerError(
|
||||||
|
rw, r,
|
||||||
|
fmt.Errorf(
|
||||||
|
"opening path %q from archive asset with id %q as fs: %w",
|
||||||
|
info.subPath, info.id, err,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
a.writePostAsset(rw, r, info.path, f)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *api) getPostAssetHandler() http.Handler {
|
func (a *api) getPostAssetHandler() http.Handler {
|
||||||
|
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
maxWidth, err := apiutil.StrToInt(r.FormValue("w"), 0)
|
archiveInfo, ok := extractPostAssetArchiveInfo(r.URL.Path)
|
||||||
if err != nil {
|
|
||||||
apiutil.BadRequest(rw, r, fmt.Errorf("invalid w parameter: %w", err))
|
if ok {
|
||||||
|
a.handleGetPostAssetArchive(rw, r, archiveInfo)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
id := filepath.Base(r.URL.Path)
|
||||||
path = strings.TrimPrefix(r.URL.Path, "/")
|
|
||||||
buf = new(bytes.Buffer)
|
|
||||||
)
|
|
||||||
|
|
||||||
err = a.params.PostAssetLoader.Load(
|
buf := new(bytes.Buffer)
|
||||||
path,
|
|
||||||
buf,
|
|
||||||
asset.LoadOpts{
|
|
||||||
ImageWidth: maxWidth,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if errors.Is(err, asset.ErrNotFound) {
|
err := a.params.PostAssetStore.Get(id, buf)
|
||||||
|
|
||||||
|
if errors.Is(err, post.ErrAssetNotFound) {
|
||||||
http.Error(rw, "Asset not found", 404)
|
http.Error(rw, "Asset not found", 404)
|
||||||
return
|
return
|
||||||
|
|
||||||
} else if errors.Is(err, asset.ErrCannotResize) {
|
|
||||||
http.Error(rw, "Image resizing not supported", 400)
|
|
||||||
return
|
|
||||||
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
apiutil.InternalServerError(
|
apiutil.InternalServerError(
|
||||||
rw, r, fmt.Errorf("fetching asset at path %q: %w", path, err),
|
rw, r, fmt.Errorf("fetching asset with id %q: %w", id, err),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.ServeContent(
|
a.writePostAsset(rw, r, id, bytes.NewReader(buf.Bytes()))
|
||||||
rw, r, path, time.Time{}, bytes.NewReader(buf.Bytes()),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +297,7 @@ func (a *api) deletePostAssetHandler() http.Handler {
|
|||||||
|
|
||||||
err := a.params.PostAssetStore.Delete(id)
|
err := a.params.PostAssetStore.Delete(id)
|
||||||
|
|
||||||
if errors.Is(err, asset.ErrNotFound) {
|
if errors.Is(err, post.ErrAssetNotFound) {
|
||||||
http.Error(rw, "Asset not found", 404)
|
http.Error(rw, "Asset not found", 404)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -20,7 +20,6 @@ import (
|
|||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset"
|
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/pow"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/pow"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
||||||
@ -36,10 +35,9 @@ type Params struct {
|
|||||||
PowManager pow.Manager
|
PowManager pow.Manager
|
||||||
Cache cache.Cache
|
Cache cache.Cache
|
||||||
|
|
||||||
PostStore post.Store
|
PostStore post.Store
|
||||||
PostAssetStore asset.Store
|
PostAssetStore post.AssetStore
|
||||||
PostAssetLoader asset.Loader
|
PostDraftStore post.DraftStore
|
||||||
PostDraftStore post.DraftStore
|
|
||||||
|
|
||||||
MailingList mailinglist.MailingList
|
MailingList mailinglist.MailingList
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ import (
|
|||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/gmi"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/gmi"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -50,7 +49,7 @@ func (a *api) postPreprocessFuncImage(args ...string) (string, error) {
|
|||||||
}{
|
}{
|
||||||
ID: id,
|
ID: id,
|
||||||
Descr: descr,
|
Descr: descr,
|
||||||
Resizable: asset.IsImageResizable(id),
|
Resizable: isImgResizable(id),
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package asset
|
package post
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -6,25 +6,23 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrNotFound is used to indicate an Asset could not be found in the
|
// ErrAssetNotFound is used to indicate an Asset could not be found in the
|
||||||
// Store.
|
// AssetStore.
|
||||||
ErrNotFound = errors.New("asset not found")
|
ErrAssetNotFound = errors.New("asset not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store implements the storage and retrieval of binary assets, which are
|
// AssetStore implements the storage and retrieval of binary assets, which are
|
||||||
// intended to be used by posts (e.g. images).
|
// intended to be used by posts (e.g. images).
|
||||||
type Store interface {
|
type AssetStore interface {
|
||||||
|
|
||||||
// Set sets the id to the contents of the given io.Reader.
|
// Set sets the id to the contents of the given io.Reader.
|
||||||
Set(id string, from io.Reader) error
|
Set(id string, from io.Reader) error
|
||||||
|
|
||||||
// Get writes the id's body to the given io.Writer, or returns
|
// Get writes the id's body to the given io.Writer, or returns
|
||||||
// ErrNotFound.
|
// ErrAssetNotFound.
|
||||||
Get(id string, into io.Writer) error
|
Get(id string, into io.Writer) error
|
||||||
|
|
||||||
// Delete's the body stored for the id, if any.
|
// Delete's the body stored for the id, if any.
|
||||||
@ -34,18 +32,18 @@ type Store interface {
|
|||||||
List() ([]string, error)
|
List() ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type store struct {
|
type assetStore struct {
|
||||||
db *post.SQLDB
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStore initializes a new Store using an existing SQLDB.
|
// NewAssetStore initializes a new AssetStore using an existing SQLDB.
|
||||||
func NewStore(db *post.SQLDB) Store {
|
func NewAssetStore(db *SQLDB) AssetStore {
|
||||||
return &store{
|
return &assetStore{
|
||||||
db: db,
|
db: db.db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) Set(id string, from io.Reader) error {
|
func (s *assetStore) Set(id string, from io.Reader) error {
|
||||||
|
|
||||||
body, err := io.ReadAll(from)
|
body, err := io.ReadAll(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -66,14 +64,14 @@ func (s *store) Set(id string, from io.Reader) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) Get(id string, into io.Writer) error {
|
func (s *assetStore) Get(id string, into io.Writer) error {
|
||||||
|
|
||||||
var body []byte
|
var body []byte
|
||||||
|
|
||||||
err := s.db.QueryRow(`SELECT body FROM assets WHERE id = ?`, id).Scan(&body)
|
err := s.db.QueryRow(`SELECT body FROM assets WHERE id = ?`, id).Scan(&body)
|
||||||
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return ErrNotFound
|
return ErrAssetNotFound
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("selecting from assets: %w", err)
|
return fmt.Errorf("selecting from assets: %w", err)
|
||||||
}
|
}
|
||||||
@ -85,12 +83,12 @@ func (s *store) Get(id string, into io.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) Delete(id string) error {
|
func (s *assetStore) Delete(id string) error {
|
||||||
_, err := s.db.Exec(`DELETE FROM assets WHERE id = ?`, id)
|
_, err := s.db.Exec(`DELETE FROM assets WHERE id = ?`, id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) List() ([]string, error) {
|
func (s *assetStore) List() ([]string, error) {
|
||||||
|
|
||||||
rows, err := s.db.Query(`SELECT id FROM assets ORDER BY id ASC`)
|
rows, err := s.db.Query(`SELECT id FROM assets ORDER BY id ASC`)
|
||||||
|
|
@ -1,43 +0,0 @@
|
|||||||
package asset
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrCannotResize = errors.New("cannot resize")
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoadOpts are optional parameters to Loader's Load method. Some may only apply
|
|
||||||
// to specific Loader implementations.
|
|
||||||
type LoadOpts struct {
|
|
||||||
|
|
||||||
// ImageWidth is used by the ImageLoader to resize images on the fly.
|
|
||||||
ImageWidth int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loader is used to load an asset and write its body into the given io.Writer.
|
|
||||||
//
|
|
||||||
// Errors:
|
|
||||||
// - ErrNotFound
|
|
||||||
// - ErrCannotResize (only if ImageLoader is used)
|
|
||||||
type Loader interface {
|
|
||||||
Load(path string, into io.Writer, opts LoadOpts) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type storeLoader struct {
|
|
||||||
store Store
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStoreLoader returns a Loader which loads assets directly from the given
|
|
||||||
// Store, with no transformation.
|
|
||||||
func NewStoreLoader(store Store) Loader {
|
|
||||||
return &storeLoader{store}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *storeLoader) Load(path string, into io.Writer, opts LoadOpts) error {
|
|
||||||
id := filepath.Base(path)
|
|
||||||
return l.store.Get(id, into)
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
package asset
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/omeid/go-tarfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type archiveLoader struct {
|
|
||||||
loader Loader
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewArchiveLoader wraps an existing Loader in order to support extracting
|
|
||||||
// files from within an archive file asset.
|
|
||||||
//
|
|
||||||
// For example, loading the path `foo.tgz/foo/bar.jpg` will call
|
|
||||||
// `Load("foo.tgz")`, load that archive into memory, and serve the file
|
|
||||||
// `./foo/bar.jpg` from within the archive.
|
|
||||||
func NewArchiveLoader(loader Loader) Loader {
|
|
||||||
return &archiveLoader{loader: loader}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *archiveLoader) Load(path string, into io.Writer, opts LoadOpts) error {
|
|
||||||
|
|
||||||
id, subPath, ok := strings.Cut(strings.TrimPrefix(path, "/"), "/")
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return l.loader.Load(path, into, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
var isGzipped bool
|
|
||||||
|
|
||||||
switch {
|
|
||||||
|
|
||||||
case strings.HasSuffix(id, ".tar.gz"),
|
|
||||||
strings.HasSuffix(id, ".tgz"):
|
|
||||||
isGzipped = true
|
|
||||||
|
|
||||||
case strings.HasSuffix(id, ".tar"):
|
|
||||||
// ok
|
|
||||||
|
|
||||||
default:
|
|
||||||
// unsupported
|
|
||||||
return l.loader.Load(path, into, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
|
|
||||||
if err := l.loader.Load(id, buf, opts); err != nil {
|
|
||||||
return fmt.Errorf("loading archive into buffer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
from io.Reader = buf
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if isGzipped {
|
|
||||||
|
|
||||||
if from, err = gzip.NewReader(from); err != nil {
|
|
||||||
return fmt.Errorf("decompressing archive asset with id %q: %w", id, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tarFS, err := tarfs.New(from)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading archive asset with id %q as fs: %w", id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := tarFS.Open(subPath)
|
|
||||||
|
|
||||||
if errors.Is(err, fs.ErrExist) {
|
|
||||||
return ErrNotFound
|
|
||||||
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"opening path %q from archive asset with id %q as fs: %w",
|
|
||||||
subPath, id, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
if _, err = io.Copy(into, f); err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"reading %q from archive asset with id %q as fs: %w",
|
|
||||||
subPath, id, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
package asset
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/jpeg"
|
|
||||||
"image/png"
|
|
||||||
"io"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/image/draw"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsImageResizable returns whether or not an image can be resized, based on its
|
|
||||||
// extension.
|
|
||||||
func IsImageResizable(path string) bool {
|
|
||||||
switch strings.ToLower(filepath.Ext(path)) {
|
|
||||||
case ".jpg", ".jpeg", ".png":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type imageLoader struct {
|
|
||||||
loader Loader
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewImageLoader wraps an existing Loader in order to perform various
|
|
||||||
// image-related transformations on any image assets being loaded. Non-image
|
|
||||||
// assets are loaded as-is.
|
|
||||||
func NewImageLoader(loader Loader) Loader {
|
|
||||||
return &imageLoader{loader}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *imageLoader) Load(path string, into io.Writer, opts LoadOpts) error {
|
|
||||||
|
|
||||||
if opts.ImageWidth == 0 {
|
|
||||||
return l.loader.Load(path, into, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !IsImageResizable(path) {
|
|
||||||
return ErrCannotResize
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
|
|
||||||
if err := l.loader.Load(path, buf, opts); err != nil {
|
|
||||||
return fmt.Errorf("loading image into buffer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
img, format, err := image.Decode(buf)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("decoding image: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
maxWidth := float64(opts.ImageWidth)
|
|
||||||
imgRect := img.Bounds()
|
|
||||||
imgW, imgH := float64(imgRect.Dx()), float64(imgRect.Dy())
|
|
||||||
|
|
||||||
if imgW > maxWidth {
|
|
||||||
|
|
||||||
newH := imgH * maxWidth / imgW
|
|
||||||
newImg := image.NewRGBA(image.Rect(0, 0, int(maxWidth), int(newH)))
|
|
||||||
|
|
||||||
// Resize
|
|
||||||
draw.BiLinear.Scale(
|
|
||||||
newImg, newImg.Bounds(), img, img.Bounds(), draw.Over, nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
img = newImg
|
|
||||||
}
|
|
||||||
|
|
||||||
switch format {
|
|
||||||
case "jpeg":
|
|
||||||
return jpeg.Encode(into, img, nil)
|
|
||||||
case "png":
|
|
||||||
return png.Encode(into, img)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown image format %q", format)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,31 +1,30 @@
|
|||||||
package asset
|
package post
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testHarness struct {
|
type assetTestHarness struct {
|
||||||
store Store
|
store AssetStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestHarness(t *testing.T) *testHarness {
|
func newAssetTestHarness(t *testing.T) *assetTestHarness {
|
||||||
|
|
||||||
db := post.NewInMemSQLDB()
|
db := NewInMemSQLDB()
|
||||||
t.Cleanup(func() { db.Close() })
|
t.Cleanup(func() { db.Close() })
|
||||||
|
|
||||||
store := NewStore(db)
|
store := NewAssetStore(db)
|
||||||
|
|
||||||
return &testHarness{
|
return &assetTestHarness{
|
||||||
store: store,
|
store: store,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *testHarness) assertGet(t *testing.T, exp, id string) {
|
func (h *assetTestHarness) assertGet(t *testing.T, exp, id string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err := h.store.Get(id, buf)
|
err := h.store.Get(id, buf)
|
||||||
@ -33,15 +32,15 @@ func (h *testHarness) assertGet(t *testing.T, exp, id string) {
|
|||||||
assert.Equal(t, exp, buf.String())
|
assert.Equal(t, exp, buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *testHarness) assertNotFound(t *testing.T, id string) {
|
func (h *assetTestHarness) assertNotFound(t *testing.T, id string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
err := h.store.Get(id, io.Discard)
|
err := h.store.Get(id, io.Discard)
|
||||||
assert.ErrorIs(t, ErrNotFound, err)
|
assert.ErrorIs(t, ErrAssetNotFound, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStore(t *testing.T) {
|
func TestAssetStore(t *testing.T) {
|
||||||
|
|
||||||
testStore := func(t *testing.T, h *testHarness) {
|
testAssetStore := func(t *testing.T, h *assetTestHarness) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
h.assertNotFound(t, "foo")
|
h.assertNotFound(t, "foo")
|
||||||
@ -86,7 +85,7 @@ func TestStore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("sql", func(t *testing.T) {
|
t.Run("sql", func(t *testing.T) {
|
||||||
h := newTestHarness(t)
|
h := newAssetTestHarness(t)
|
||||||
testStore(t, h)
|
testAssetStore(t, h)
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -47,7 +47,7 @@ func (s *draftStore) Set(post Post) error {
|
|||||||
return fmt.Errorf("json marshaling tags %#v: %w", post.Tags, err)
|
return fmt.Errorf("json marshaling tags %#v: %w", post.Tags, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.db.Exec(
|
_, err = s.db.db.Exec(
|
||||||
`INSERT INTO post_drafts (
|
`INSERT INTO post_drafts (
|
||||||
id, title, description, tags, series, body, format
|
id, title, description, tags, series, body, format
|
||||||
)
|
)
|
||||||
@ -145,7 +145,7 @@ func (s *draftStore) get(
|
|||||||
|
|
||||||
func (s *draftStore) Get(page, count int) ([]Post, bool, error) {
|
func (s *draftStore) Get(page, count int) ([]Post, bool, error) {
|
||||||
|
|
||||||
posts, err := s.get(s.db, count+1, page*count, ``)
|
posts, err := s.get(s.db.db, count+1, page*count, ``)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("querying post_drafts: %w", err)
|
return nil, false, fmt.Errorf("querying post_drafts: %w", err)
|
||||||
@ -163,7 +163,7 @@ func (s *draftStore) Get(page, count int) ([]Post, bool, error) {
|
|||||||
|
|
||||||
func (s *draftStore) GetByID(id string) (Post, error) {
|
func (s *draftStore) GetByID(id string) (Post, error) {
|
||||||
|
|
||||||
posts, err := s.get(s.db, 0, 0, `WHERE p.id=?`, id)
|
posts, err := s.get(s.db.db, 0, 0, `WHERE p.id=?`, id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Post{}, fmt.Errorf("querying post_drafts: %w", err)
|
return Post{}, fmt.Errorf("querying post_drafts: %w", err)
|
||||||
@ -182,7 +182,7 @@ func (s *draftStore) GetByID(id string) (Post, error) {
|
|||||||
|
|
||||||
func (s *draftStore) Delete(id string) error {
|
func (s *draftStore) Delete(id string) error {
|
||||||
|
|
||||||
if _, err := s.db.Exec(`DELETE FROM post_drafts WHERE id = ?`, id); err != nil {
|
if _, err := s.db.db.Exec(`DELETE FROM post_drafts WHERE id = ?`, id); err != nil {
|
||||||
return fmt.Errorf("deleting from post_drafts: %w", err)
|
return fmt.Errorf("deleting from post_drafts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ func (s *store) Set(post Post, now time.Time) (bool, error) {
|
|||||||
|
|
||||||
var first bool
|
var first bool
|
||||||
|
|
||||||
err := s.db.WithTx(func(tx *sql.Tx) error {
|
err := s.db.withTx(func(tx *sql.Tx) error {
|
||||||
|
|
||||||
nowTS := now.Unix()
|
nowTS := now.Unix()
|
||||||
|
|
||||||
@ -244,7 +244,7 @@ func (s *store) get(
|
|||||||
|
|
||||||
func (s *store) Get(page, count int) ([]StoredPost, bool, error) {
|
func (s *store) Get(page, count int) ([]StoredPost, bool, error) {
|
||||||
|
|
||||||
posts, err := s.get(s.db, count+1, page*count, ``)
|
posts, err := s.get(s.db.db, count+1, page*count, ``)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("querying posts: %w", err)
|
return nil, false, fmt.Errorf("querying posts: %w", err)
|
||||||
@ -262,7 +262,7 @@ func (s *store) Get(page, count int) ([]StoredPost, bool, error) {
|
|||||||
|
|
||||||
func (s *store) GetByID(id string) (StoredPost, error) {
|
func (s *store) GetByID(id string) (StoredPost, error) {
|
||||||
|
|
||||||
posts, err := s.get(s.db, 0, 0, `WHERE p.id=?`, id)
|
posts, err := s.get(s.db.db, 0, 0, `WHERE p.id=?`, id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return StoredPost{}, fmt.Errorf("querying posts: %w", err)
|
return StoredPost{}, fmt.Errorf("querying posts: %w", err)
|
||||||
@ -280,14 +280,14 @@ func (s *store) GetByID(id string) (StoredPost, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) GetBySeries(series string) ([]StoredPost, error) {
|
func (s *store) GetBySeries(series string) ([]StoredPost, error) {
|
||||||
return s.get(s.db, 0, 0, `WHERE p.series=?`, series)
|
return s.get(s.db.db, 0, 0, `WHERE p.series=?`, series)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) GetByTag(tag string) ([]StoredPost, error) {
|
func (s *store) GetByTag(tag string) ([]StoredPost, error) {
|
||||||
|
|
||||||
var posts []StoredPost
|
var posts []StoredPost
|
||||||
|
|
||||||
err := s.db.WithTx(func(tx *sql.Tx) error {
|
err := s.db.withTx(func(tx *sql.Tx) error {
|
||||||
|
|
||||||
rows, err := tx.Query(`SELECT post_id FROM post_tags WHERE tag = ?`, tag)
|
rows, err := tx.Query(`SELECT post_id FROM post_tags WHERE tag = ?`, tag)
|
||||||
|
|
||||||
@ -331,7 +331,7 @@ func (s *store) GetByTag(tag string) ([]StoredPost, error) {
|
|||||||
|
|
||||||
func (s *store) GetTags() ([]string, error) {
|
func (s *store) GetTags() ([]string, error) {
|
||||||
|
|
||||||
rows, err := s.db.Query(`SELECT tag FROM post_tags GROUP BY tag`)
|
rows, err := s.db.db.Query(`SELECT tag FROM post_tags GROUP BY tag`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("querying all tags: %w", err)
|
return nil, fmt.Errorf("querying all tags: %w", err)
|
||||||
}
|
}
|
||||||
@ -355,7 +355,7 @@ func (s *store) GetTags() ([]string, error) {
|
|||||||
|
|
||||||
func (s *store) Delete(id string) error {
|
func (s *store) Delete(id string) error {
|
||||||
|
|
||||||
return s.db.WithTx(func(tx *sql.Tx) error {
|
return s.db.withTx(func(tx *sql.Tx) error {
|
||||||
|
|
||||||
if _, err := tx.Exec(`DELETE FROM post_tags WHERE post_id = ?`, id); err != nil {
|
if _, err := tx.Exec(`DELETE FROM post_tags WHERE post_id = ?`, id); err != nil {
|
||||||
return fmt.Errorf("deleting from post_tags: %w", err)
|
return fmt.Errorf("deleting from post_tags: %w", err)
|
||||||
|
@ -78,7 +78,7 @@ var migrations = &migrate.MemoryMigrationSource{Migrations: []*migrate.Migration
|
|||||||
// SQLDB is a sqlite3 database which can be used by storage interfaces within
|
// SQLDB is a sqlite3 database which can be used by storage interfaces within
|
||||||
// this package.
|
// this package.
|
||||||
type SQLDB struct {
|
type SQLDB struct {
|
||||||
*sql.DB
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSQLDB initializes and returns a new sqlite3 database for storage
|
// NewSQLDB initializes and returns a new sqlite3 database for storage
|
||||||
@ -116,14 +116,12 @@ func NewInMemSQLDB() *SQLDB {
|
|||||||
|
|
||||||
// Close cleans up loose resources being held by the db.
|
// Close cleans up loose resources being held by the db.
|
||||||
func (db *SQLDB) Close() error {
|
func (db *SQLDB) Close() error {
|
||||||
return db.DB.Close()
|
return db.db.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithTx initializes a transaction, runs the callback using it, and either
|
func (db *SQLDB) withTx(cb func(*sql.Tx) error) error {
|
||||||
// commits or rolls it back depending on if the callback returns an error.
|
|
||||||
func (db *SQLDB) WithTx(cb func(*sql.Tx) error) error {
|
|
||||||
|
|
||||||
tx, err := db.DB.Begin()
|
tx, err := db.db.Begin()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("starting transaction: %w", err)
|
return fmt.Errorf("starting transaction: %w", err)
|
||||||
|
Loading…
Reference in New Issue
Block a user