Compare commits
6 commits
97112d99dd
...
53d9b62174
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53d9b62174 | ||
|
|
199ff9feb9 | ||
|
|
d0cebe6564 | ||
|
|
48f39133d7 | ||
|
|
0a9af9cde8 | ||
|
|
6b697e008f |
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,3 +5,4 @@ static/assets/
|
||||||
# Local Netlify folder
|
# Local Netlify folder
|
||||||
.netlify
|
.netlify
|
||||||
.env
|
.env
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
6
Makefile
6
Makefile
|
|
@ -26,3 +26,9 @@ build: frontend
|
||||||
.Phony: run
|
.Phony: run
|
||||||
run: build
|
run: build
|
||||||
./build/weiro
|
./build/weiro
|
||||||
|
|
||||||
|
.Phony: setup_targets
|
||||||
|
setup_targets: build
|
||||||
|
SITE_ID=$$(DATA_DIR=$(BUILD_DIR)/data ./$(BUILD_DIR)/weiro sites | tail -n1 | awk '{ print $$1 }'); \
|
||||||
|
DATA_DIR=$(BUILD_DIR)/data ./build/weiro pubtargets "$$SITE_ID"; \
|
||||||
|
DATA_DIR=$(BUILD_DIR)/data ./build/weiro pubtargets add --site "$$SITE_ID" --type localfs --ref ./$(BUILD_DIR)/out --url http://localhost:8000
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,19 @@ $container-max-widths: (
|
||||||
.post-form textarea {
|
.post-form textarea {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.postlist .post img {
|
||||||
|
max-width: 300px;
|
||||||
|
height: auto;
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-upload figure img {
|
||||||
|
max-width: 100vw;
|
||||||
|
height: auto;
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progressbar {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
41
assets/js/controllers/show_upload.js
Normal file
41
assets/js/controllers/show_upload.js
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import {showToast} from "../services/toast";
|
||||||
|
|
||||||
|
export default class ShowUploadController extends Controller {
|
||||||
|
static values = {
|
||||||
|
copySnippet: String,
|
||||||
|
siteId: Number,
|
||||||
|
uploadId: Number,
|
||||||
|
};
|
||||||
|
|
||||||
|
async copy(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(this.copySnippetValue);
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
title: "️📋 HTML Snippet",
|
||||||
|
body: "Copied to clipboard.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (!confirm("Are you sure you want to delete this upload?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this._doDelete();
|
||||||
|
window.location = `/sites/${this.siteIdValue}/uploads/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _doDelete() {
|
||||||
|
const url = `/sites/${this.siteIdValue}/uploads/${this.uploadIdValue}`
|
||||||
|
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
146
assets/js/controllers/upload.js
Normal file
146
assets/js/controllers/upload.js
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class UploadController extends Controller {
|
||||||
|
static targets = [
|
||||||
|
'uploadBtn',
|
||||||
|
'progressbar',
|
||||||
|
'progressbarProgress',
|
||||||
|
];
|
||||||
|
|
||||||
|
static values = {
|
||||||
|
siteId: Number,
|
||||||
|
};
|
||||||
|
|
||||||
|
upload(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
this._promptForUpload((files) => {
|
||||||
|
files.sort((a, b) => b.lastModified - a.lastModified);
|
||||||
|
this._doUploads(files);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_promptForUpload(onAccept) {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'image/*';
|
||||||
|
input.multiple = true;
|
||||||
|
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const files = Array.from(e.target.files);
|
||||||
|
if (files.length > 0) {
|
||||||
|
onAccept(files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _doUploads(files) {
|
||||||
|
this.uploadBtnTarget.disabled = true;
|
||||||
|
this._showUploadProgressBar();
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
await this._doUpload(files[i], i, files.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _doUpload(file, thisFileIndex, nFiles) {
|
||||||
|
console.log(`Uploading ${file.name}: new pending`);
|
||||||
|
|
||||||
|
// Prepare upload of file supplying size and mime-type
|
||||||
|
let newPending = await (await fetch(`/sites/${this.siteIdValue}/uploads/pending`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
size: file.size,
|
||||||
|
mime: file.type,
|
||||||
|
name: file.name,
|
||||||
|
})
|
||||||
|
})).json();
|
||||||
|
|
||||||
|
// Upload file in 2 MB blocks
|
||||||
|
let offset = 0;
|
||||||
|
let chunkSize = 2 * 1024 * 1024;
|
||||||
|
while (offset < file.size) {
|
||||||
|
let chunk = file.slice(offset, offset + chunkSize);
|
||||||
|
|
||||||
|
console.log(`Uploading ${file.name}: uploading part`);
|
||||||
|
await this._uploadChunk(`/sites/${this.siteIdValue}/uploads/pending/${newPending.guid}`, chunk, {
|
||||||
|
chunkOffset: offset,
|
||||||
|
totalSize: file.size,
|
||||||
|
thisFileIndex,
|
||||||
|
nFiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
offset += chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate SHA256 hash
|
||||||
|
const hash = await this._calculateSHA256(file);
|
||||||
|
|
||||||
|
// Finalise upload
|
||||||
|
console.log(`Uploading ${file.name}: finalise`);
|
||||||
|
await fetch(`/sites/${this.siteIdValue}/uploads/pending/${newPending.guid}/finalize`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
hash: hash
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_uploadChunk(url, chunk, progressInfo) {
|
||||||
|
let { chunkOffset, totalSize, thisFileIndex, nFiles } = progressInfo;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const bytesUploaded = chunkOffset + e.loaded;
|
||||||
|
const fractionalCompleteOfThisFile = +bytesUploaded / +totalSize;
|
||||||
|
const percentComplete = (thisFileIndex + fractionalCompleteOfThisFile) * 100 / nFiles;
|
||||||
|
console.log(`Uploading ${chunk.name}: ${percentComplete.toFixed(2)}%`);
|
||||||
|
this.progressbarProgressTarget.style.width = `${percentComplete}%`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Upload failed with status ${xhr.status}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('error', () => reject(new Error('Upload failed')));
|
||||||
|
xhr.addEventListener('abort', () => reject(new Error('Upload aborted')));
|
||||||
|
|
||||||
|
xhr.open('POST', url);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||||
|
xhr.send(chunk);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_showUploadProgressBar() {
|
||||||
|
this.progressbarTarget.classList.remove('d-none');
|
||||||
|
this.progressbarProgressTarget.style.width = '0%';
|
||||||
|
}
|
||||||
|
|
||||||
|
async _calculateSHA256(file) {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,8 @@ import PostlistController from "./controllers/postlist";
|
||||||
import PosteditController from "./controllers/postedit";
|
import PosteditController from "./controllers/postedit";
|
||||||
import LogoutController from "./controllers/logout";
|
import LogoutController from "./controllers/logout";
|
||||||
import FirstRunController from "./controllers/firstrun";
|
import FirstRunController from "./controllers/firstrun";
|
||||||
|
import UploadController from "./controllers/upload";
|
||||||
|
import ShowUploadController from "./controllers/show_upload";
|
||||||
|
|
||||||
window.Stimulus = Application.start()
|
window.Stimulus = Application.start()
|
||||||
Stimulus.register("toast", ToastController);
|
Stimulus.register("toast", ToastController);
|
||||||
|
|
@ -12,3 +14,5 @@ Stimulus.register("postlist", PostlistController);
|
||||||
Stimulus.register("postedit", PosteditController);
|
Stimulus.register("postedit", PosteditController);
|
||||||
Stimulus.register("logout", LogoutController);
|
Stimulus.register("logout", LogoutController);
|
||||||
Stimulus.register("first-run", FirstRunController);
|
Stimulus.register("first-run", FirstRunController);
|
||||||
|
Stimulus.register("upload", UploadController);
|
||||||
|
Stimulus.register("show-upload", ShowUploadController);
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
|
@ -17,11 +16,11 @@ import (
|
||||||
fiber_html "github.com/gofiber/template/html/v3"
|
fiber_html "github.com/gofiber/template/html/v3"
|
||||||
"github.com/gofiber/utils/v2"
|
"github.com/gofiber/utils/v2"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/yuin/goldmark"
|
|
||||||
"github.com/yuin/goldmark/extension"
|
|
||||||
"lmika.dev/lmika/weiro/config"
|
"lmika.dev/lmika/weiro/config"
|
||||||
"lmika.dev/lmika/weiro/handlers"
|
"lmika.dev/lmika/weiro/handlers"
|
||||||
"lmika.dev/lmika/weiro/handlers/middleware"
|
"lmika.dev/lmika/weiro/handlers/middleware"
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
"lmika.dev/lmika/weiro/providers/markdown"
|
||||||
"lmika.dev/lmika/weiro/services"
|
"lmika.dev/lmika/weiro/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -49,16 +48,19 @@ Starting weiro without any arguments will start the server.
|
||||||
|
|
||||||
fiberTemplate := fiber_html.New("./views", ".html")
|
fiberTemplate := fiber_html.New("./views", ".html")
|
||||||
fiberTemplate.Funcmap["sub"] = func(x, y int) int { return x - y }
|
fiberTemplate.Funcmap["sub"] = func(x, y int) int { return x - y }
|
||||||
fiberTemplate.Funcmap["markdown"] = func() func(s string) template.HTML {
|
fiberTemplate.Funcmap["markdown"] = func() func(s string, site models.Site) template.HTML {
|
||||||
mdParser := goldmark.New(
|
mdParser := markdown.NewRendererForUI()
|
||||||
goldmark.WithExtensions(extension.GFM),
|
return func(s string, site models.Site) template.HTML {
|
||||||
)
|
ctx := context.Background()
|
||||||
return func(s string) template.HTML {
|
if site.ID != 0 {
|
||||||
var sb strings.Builder
|
ctx = models.WithSite(ctx, site)
|
||||||
if err := mdParser.Convert([]byte(s), &sb); err != nil {
|
}
|
||||||
|
|
||||||
|
s, err := mdParser.Render(ctx, s)
|
||||||
|
if err != nil {
|
||||||
return template.HTML("Markdown error: " + html.EscapeString(err.Error()))
|
return template.HTML("Markdown error: " + html.EscapeString(err.Error()))
|
||||||
}
|
}
|
||||||
return template.HTML(sb.String())
|
return template.HTML(s)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -108,6 +110,7 @@ Starting weiro without any arguments will start the server.
|
||||||
ih := handlers.IndexHandler{SiteService: svcs.Sites}
|
ih := handlers.IndexHandler{SiteService: svcs.Sites}
|
||||||
lh := handlers.LoginHandler{Config: cfg, AuthService: svcs.Auth}
|
lh := handlers.LoginHandler{Config: cfg, AuthService: svcs.Auth}
|
||||||
ph := handlers.PostsHandler{PostService: svcs.Posts}
|
ph := handlers.PostsHandler{PostService: svcs.Posts}
|
||||||
|
uh := handlers.UploadsHandler{UploadsService: svcs.Uploads}
|
||||||
|
|
||||||
app.Get("/login", lh.Login)
|
app.Get("/login", lh.Login)
|
||||||
app.Post("/login", lh.DoLogin)
|
app.Post("/login", lh.DoLogin)
|
||||||
|
|
@ -122,6 +125,15 @@ Starting weiro without any arguments will start the server.
|
||||||
siteGroup.Patch("/posts/:postID", ph.Patch)
|
siteGroup.Patch("/posts/:postID", ph.Patch)
|
||||||
siteGroup.Delete("/posts/:postID", ph.Delete)
|
siteGroup.Delete("/posts/:postID", ph.Delete)
|
||||||
|
|
||||||
|
siteGroup.Get("/uploads", uh.Index)
|
||||||
|
siteGroup.Get("/uploads/slug/+", uh.ShowFromSlug)
|
||||||
|
siteGroup.Get("/uploads/:uploadID", uh.Show)
|
||||||
|
siteGroup.Get("/uploads/:uploadID/raw", uh.ShowRaw)
|
||||||
|
siteGroup.Post("/uploads/pending", uh.New)
|
||||||
|
siteGroup.Post("/uploads/pending/:guid", uh.UploadPart)
|
||||||
|
siteGroup.Post("/uploads/pending/:guid/finalize", uh.UploadComplete)
|
||||||
|
siteGroup.Delete("/uploads/:uploadID", uh.Delete)
|
||||||
|
|
||||||
app.Get("/", middleware.OptionalUser(svcs.Auth), ih.Index)
|
app.Get("/", middleware.OptionalUser(svcs.Auth), ih.Index)
|
||||||
app.Get("/first-run", ih.FirstRun)
|
app.Get("/first-run", ih.FirstRun)
|
||||||
app.Post("/first-run", ih.FirstRunSubmit)
|
app.Post("/first-run", ih.FirstRunSubmit)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DataDir string `env:"DATA_DIR"`
|
DataDir string `env:"DATA_DIR"`
|
||||||
|
ScratchDir string `env:"SCRATCH_DIR"`
|
||||||
SiteDomain string `env:"SITE_DOMAIN"`
|
SiteDomain string `env:"SITE_DOMAIN"`
|
||||||
LoginLocked bool `env:"LOGIN_LOCKED,default=false"`
|
LoginLocked bool `env:"LOGIN_LOCKED,default=false"`
|
||||||
Env string `env:"ENV,default=prod"`
|
Env string `env:"ENV,default=prod"`
|
||||||
|
|
|
||||||
10
go.mod
10
go.mod
|
|
@ -19,13 +19,18 @@ require (
|
||||||
github.com/Azure/go-autorest/logger v0.1.0 // indirect
|
github.com/Azure/go-autorest/logger v0.1.0 // indirect
|
||||||
github.com/Azure/go-autorest/tracing v0.5.0 // indirect
|
github.com/Azure/go-autorest/tracing v0.5.0 // indirect
|
||||||
github.com/Netflix/go-env v0.1.2 // indirect
|
github.com/Netflix/go-env v0.1.2 // indirect
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0 // indirect
|
||||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
|
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/barasher/go-exiftool v1.10.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.0.2 // indirect
|
github.com/cenkalti/backoff/v4 v4.0.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||||
|
github.com/disintegration/imaging v1.6.2 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/go-openapi/analysis v0.19.16 // indirect
|
github.com/go-openapi/analysis v0.19.16 // indirect
|
||||||
github.com/go-openapi/errors v0.19.9 // indirect
|
github.com/go-openapi/errors v0.19.9 // indirect
|
||||||
|
|
@ -47,6 +52,7 @@ require (
|
||||||
github.com/gofiber/template/v2 v2.1.0 // indirect
|
github.com/gofiber/template/v2 v2.1.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
|
|
@ -57,6 +63,7 @@ require (
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.33 // indirect
|
github.com/mattn/go-sqlite3 v1.14.33 // indirect
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.4.0 // indirect
|
github.com/mitchellh/mapstructure v1.4.0 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/netlify/open-api/v2 v2.49.1 // indirect
|
github.com/netlify/open-api/v2 v2.49.1 // indirect
|
||||||
|
|
@ -76,10 +83,13 @@ require (
|
||||||
go.uber.org/multierr v1.6.0 // indirect
|
go.uber.org/multierr v1.6.0 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
|
||||||
golang.org/x/net v0.50.0 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
lmika.dev/pkg/modash v0.1.1-0.20260302110707-31c6b125c997 // indirect
|
||||||
modernc.org/libc v1.67.6 // indirect
|
modernc.org/libc v1.67.6 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|
|
||||||
61
go.sum
61
go.sum
|
|
@ -34,6 +34,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
||||||
github.com/Netflix/go-env v0.1.2 h1:0DRoLR9lECQ9Zqvkswuebm3jJ/2enaDX6Ei8/Z+EnK0=
|
github.com/Netflix/go-env v0.1.2 h1:0DRoLR9lECQ9Zqvkswuebm3jJ/2enaDX6Ei8/Z+EnK0=
|
||||||
github.com/Netflix/go-env v0.1.2/go.mod h1:WlIhYi++8FlKNJtrop1mjXYAJMzv1f43K4MqCoh0yGE=
|
github.com/Netflix/go-env v0.1.2/go.mod h1:WlIhYi++8FlKNJtrop1mjXYAJMzv1f43K4MqCoh0yGE=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||||
|
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
|
@ -47,6 +49,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
|
@ -57,6 +61,10 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:o
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
|
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
|
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
|
github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs=
|
||||||
|
github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
|
@ -81,6 +89,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
|
@ -255,6 +265,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
|
@ -268,6 +279,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
|
@ -355,6 +368,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
|
@ -505,6 +520,10 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh
|
||||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
|
@ -518,6 +537,8 @@ golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
|
@ -532,6 +553,10 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
|
@ -559,8 +584,14 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
|
|
@ -577,8 +608,15 @@ golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
|
@ -608,15 +646,26 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
|
@ -625,6 +674,11 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
|
@ -660,6 +714,9 @@ golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtn
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200612220849-54c614fe050c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200612220849-54c614fe050c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
|
|
@ -731,6 +788,10 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
lmika.dev/pkg/litemigrate v0.1.0 h1:DBEJahbQO7W3uEmAOQGg1URBWYimg0ClWHi83M2MZwk=
|
lmika.dev/pkg/litemigrate v0.1.0 h1:DBEJahbQO7W3uEmAOQGg1URBWYimg0ClWHi83M2MZwk=
|
||||||
lmika.dev/pkg/litemigrate v0.1.0/go.mod h1:GQWWDiMZGQaVspcwKNq8vIBPN5H+KsUo/VBIeh9OfLg=
|
lmika.dev/pkg/litemigrate v0.1.0/go.mod h1:GQWWDiMZGQaVspcwKNq8vIBPN5H+KsUo/VBIeh9OfLg=
|
||||||
|
lmika.dev/pkg/modash v0.1.0 h1:fltroSvP0nKj9K0E6G+S9LULvB9Qhj47+SZ2b9v/v/c=
|
||||||
|
lmika.dev/pkg/modash v0.1.0/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
||||||
|
lmika.dev/pkg/modash v0.1.1-0.20260302110707-31c6b125c997 h1:XGdi5Ca5IJgGXPd057R2QHENQ6PwIUUfhBTGGF6yuLM=
|
||||||
|
lmika.dev/pkg/modash v0.1.1-0.20260302110707-31c6b125c997/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
|
@ -92,8 +91,6 @@ func (ph PostsHandler) Update(c fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ph PostsHandler) Patch(c fiber.Ctx) error {
|
func (ph PostsHandler) Patch(c fiber.Ctx) error {
|
||||||
log.Println("PATCH")
|
|
||||||
|
|
||||||
postIDStr := c.Params("postID")
|
postIDStr := c.Params("postID")
|
||||||
if postIDStr == "" {
|
if postIDStr == "" {
|
||||||
return fiber.ErrBadRequest
|
return fiber.ErrBadRequest
|
||||||
|
|
@ -110,8 +107,6 @@ func (ph PostsHandler) Patch(c fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Request")
|
|
||||||
|
|
||||||
switch req.Action {
|
switch req.Action {
|
||||||
case "restore":
|
case "restore":
|
||||||
if err := ph.PostService.RestorePost(c.Context(), postID); err != nil {
|
if err := ph.PostService.RestorePost(c.Context(), postID); err != nil {
|
||||||
|
|
|
||||||
164
handlers/uploads.go
Normal file
164
handlers/uploads.go
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
"lmika.dev/lmika/weiro/services/uploads"
|
||||||
|
"lmika.dev/pkg/modash/moslice"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UploadsHandler struct {
|
||||||
|
UploadsService *uploads.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uh UploadsHandler) Index(c fiber.Ctx) error {
|
||||||
|
uploads, err := uh.UploadsService.ListUploads(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := moslice.Batch(uploads, 5)
|
||||||
|
|
||||||
|
return c.Render("uploads/index", fiber.Map{"uploads": rows})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uh UploadsHandler) Show(c fiber.Ctx) error {
|
||||||
|
uploadIDStr := c.Params("uploadID")
|
||||||
|
if uploadIDStr == "" {
|
||||||
|
return fiber.ErrBadRequest
|
||||||
|
}
|
||||||
|
uploadID, err := strconv.ParseInt(uploadIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.ErrBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
upload, err := uh.UploadsService.FetchUpload(c.Context(), uploadID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render("uploads/show", fiber.Map{"upload": upload})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uh UploadsHandler) Delete(c fiber.Ctx) error {
|
||||||
|
uploadIDStr := c.Params("uploadID")
|
||||||
|
if uploadIDStr == "" {
|
||||||
|
return fiber.ErrBadRequest
|
||||||
|
}
|
||||||
|
uploadID, err := strconv.ParseInt(uploadIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.ErrBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := uh.UploadsService.DeleteUpload(c.Context(), uploadID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect().To(fmt.Sprintf("/sites/%v/uploads", models.MustGetSite(c.Context()).ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uh UploadsHandler) ShowRaw(c fiber.Ctx) error {
|
||||||
|
uploadIDStr := c.Params("uploadID")
|
||||||
|
if uploadIDStr == "" {
|
||||||
|
return fiber.ErrBadRequest
|
||||||
|
}
|
||||||
|
uploadID, err := strconv.ParseInt(uploadIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.ErrBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
upload, rwFn, err := uh.UploadsService.OpenUpload(c.Context(), uploadID)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uh.serveUpload(c, upload, rwFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uh UploadsHandler) ShowFromSlug(c fiber.Ctx) error {
|
||||||
|
uploadSlug := c.Params("+")
|
||||||
|
if uploadSlug == "" {
|
||||||
|
return fiber.ErrBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
upload, rwFn, err := uh.UploadsService.OpenUploadFromSlug(c.Context(), uploadSlug)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uh.serveUpload(c, upload, rwFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uh UploadsHandler) serveUpload(c fiber.Ctx, upload models.Upload, uploadReader func() (io.ReadCloser, error)) error {
|
||||||
|
c.Set("Content-Type", upload.MIMEType)
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
return c.SendStreamWriter(func(w *bufio.Writer) {
|
||||||
|
rw, err := uploadReader()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rw.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(w, rw)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uh UploadsHandler) New(c fiber.Ctx) error {
|
||||||
|
var req uploads.NewPendingRequest
|
||||||
|
|
||||||
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := uh.UploadsService.NewPending(c.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uh UploadsHandler) UploadPart(c fiber.Ctx) error {
|
||||||
|
guid := c.Params("guid")
|
||||||
|
if guid == "" {
|
||||||
|
return fiber.ErrBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := uh.UploadsService.WriteToPending(c.Context(), guid, c.Body()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusAccepted).JSON(fiber.Map{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uh UploadsHandler) UploadComplete(c fiber.Ctx) error {
|
||||||
|
guid := c.Params("guid")
|
||||||
|
if guid == "" {
|
||||||
|
return fiber.ErrBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
var res struct {
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
if err := c.Bind().Body(&res); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := uh.UploadsService.FinalizePending(c.Context(), guid, res.Hash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusAccepted).JSON(fiber.Map{})
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ type userKeyType struct{}
|
||||||
type siteKeyType struct{}
|
type siteKeyType struct{}
|
||||||
|
|
||||||
var userKey = userKeyType{}
|
var userKey = userKeyType{}
|
||||||
var siteKey = userKeyType{}
|
var siteKey = siteKeyType{}
|
||||||
|
|
||||||
func WithUser(ctx context.Context, user User) context.Context {
|
func WithUser(ctx context.Context, user User) context.Context {
|
||||||
return context.WithValue(ctx, userKey, user)
|
return context.WithValue(ctx, userKey, user)
|
||||||
|
|
@ -25,3 +25,8 @@ func GetSite(ctx context.Context) (Site, bool) {
|
||||||
site, ok := ctx.Value(siteKey).(Site)
|
site, ok := ctx.Value(siteKey).(Site)
|
||||||
return site, ok
|
return site, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MustGetSite(ctx context.Context) Site {
|
||||||
|
site, _ := GetSite(ctx)
|
||||||
|
return site
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
package pubmodel
|
package pubmodel
|
||||||
|
|
||||||
import "lmika.dev/lmika/weiro/models"
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
type Site struct {
|
type Site struct {
|
||||||
models.Site
|
models.Site
|
||||||
BaseURL string
|
BaseURL string
|
||||||
Posts []*models.Post
|
Posts []*models.Post
|
||||||
|
Uploads []models.Upload
|
||||||
|
|
||||||
|
OpenUpload func(u models.Upload) (io.ReadCloser, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
models/uploads.go
Normal file
25
models/uploads.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Upload struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
SiteID int64 `json:"site_id"`
|
||||||
|
GUID string `json:"guid"`
|
||||||
|
FileSize int64 `json:"file_size"`
|
||||||
|
MIMEType string `json:"mime_type"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Alt string `json:"alt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PendingUpload struct {
|
||||||
|
GUID string `json:"guid"`
|
||||||
|
SiteID int64 `json:"site_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
FileSize int64 `json:"file_size"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
MIMEType string `json:"mime_mime"`
|
||||||
|
UploadStarted time.Time `json:"upload_started"`
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,17 @@
|
||||||
|
|
||||||
package sqlgen
|
package sqlgen
|
||||||
|
|
||||||
|
type PendingUpload struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
Guid string
|
||||||
|
UserID int64
|
||||||
|
Filename string
|
||||||
|
FileSize int64
|
||||||
|
MimeType string
|
||||||
|
UploadStartedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
type Post struct {
|
type Post struct {
|
||||||
ID int64
|
ID int64
|
||||||
SiteID int64
|
SiteID int64
|
||||||
|
|
@ -38,6 +49,18 @@ type Site struct {
|
||||||
CreatedAt int64
|
CreatedAt int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Upload struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
Guid string
|
||||||
|
MimeType string
|
||||||
|
Filename string
|
||||||
|
FileSize int64
|
||||||
|
Slug string
|
||||||
|
Alt string
|
||||||
|
CreatedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64
|
ID int64
|
||||||
Username string
|
Username string
|
||||||
|
|
|
||||||
77
providers/db/gen/sqlgen/pending_uploads.sql.go
Normal file
77
providers/db/gen/sqlgen/pending_uploads.sql.go
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: pending_uploads.sql
|
||||||
|
|
||||||
|
package sqlgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const deletePendingUpload = `-- name: DeletePendingUpload :exec
|
||||||
|
DELETE FROM pending_uploads WHERE guid = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeletePendingUpload(ctx context.Context, guid string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deletePendingUpload, guid)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertPendingUpload = `-- name: InsertPendingUpload :one
|
||||||
|
INSERT INTO pending_uploads (
|
||||||
|
site_id,
|
||||||
|
guid,
|
||||||
|
user_id,
|
||||||
|
filename,
|
||||||
|
file_size,
|
||||||
|
mime_type,
|
||||||
|
upload_started_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
RETURNING id
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertPendingUploadParams struct {
|
||||||
|
SiteID int64
|
||||||
|
Guid string
|
||||||
|
UserID int64
|
||||||
|
Filename string
|
||||||
|
FileSize int64
|
||||||
|
MimeType string
|
||||||
|
UploadStartedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertPendingUpload(ctx context.Context, arg InsertPendingUploadParams) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, insertPendingUpload,
|
||||||
|
arg.SiteID,
|
||||||
|
arg.Guid,
|
||||||
|
arg.UserID,
|
||||||
|
arg.Filename,
|
||||||
|
arg.FileSize,
|
||||||
|
arg.MimeType,
|
||||||
|
arg.UploadStartedAt,
|
||||||
|
)
|
||||||
|
var id int64
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectPendingUploadByGUID = `-- name: SelectPendingUploadByGUID :one
|
||||||
|
SELECT id, site_id, guid, user_id, filename, file_size, mime_type, upload_started_at FROM pending_uploads WHERE guid = ? LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) SelectPendingUploadByGUID(ctx context.Context, guid string) (PendingUpload, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, selectPendingUploadByGUID, guid)
|
||||||
|
var i PendingUpload
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SiteID,
|
||||||
|
&i.Guid,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Filename,
|
||||||
|
&i.FileSize,
|
||||||
|
&i.MimeType,
|
||||||
|
&i.UploadStartedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
156
providers/db/gen/sqlgen/uploads.sql.go
Normal file
156
providers/db/gen/sqlgen/uploads.sql.go
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: uploads.sql
|
||||||
|
|
||||||
|
package sqlgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteUpload = `-- name: DeleteUpload :exec
|
||||||
|
DELETE FROM uploads WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteUpload(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteUpload, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertUpload = `-- name: InsertUpload :exec
|
||||||
|
INSERT INTO uploads (
|
||||||
|
site_id,
|
||||||
|
guid,
|
||||||
|
mime_type,
|
||||||
|
filename,
|
||||||
|
file_size,
|
||||||
|
slug,
|
||||||
|
alt,
|
||||||
|
created_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
RETURNING id
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUploadParams struct {
|
||||||
|
SiteID int64
|
||||||
|
Guid string
|
||||||
|
MimeType string
|
||||||
|
Filename string
|
||||||
|
FileSize int64
|
||||||
|
Slug string
|
||||||
|
Alt string
|
||||||
|
CreatedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertUpload(ctx context.Context, arg InsertUploadParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, insertUpload,
|
||||||
|
arg.SiteID,
|
||||||
|
arg.Guid,
|
||||||
|
arg.MimeType,
|
||||||
|
arg.Filename,
|
||||||
|
arg.FileSize,
|
||||||
|
arg.Slug,
|
||||||
|
arg.Alt,
|
||||||
|
arg.CreatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectUploadByID = `-- name: SelectUploadByID :one
|
||||||
|
SELECT id, site_id, guid, mime_type, filename, file_size, slug, alt, created_at FROM uploads WHERE id = ? LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) SelectUploadByID(ctx context.Context, id int64) (Upload, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, selectUploadByID, id)
|
||||||
|
var i Upload
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SiteID,
|
||||||
|
&i.Guid,
|
||||||
|
&i.MimeType,
|
||||||
|
&i.Filename,
|
||||||
|
&i.FileSize,
|
||||||
|
&i.Slug,
|
||||||
|
&i.Alt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectUploadBySiteIDAndSlug = `-- name: SelectUploadBySiteIDAndSlug :one
|
||||||
|
SELECT id, site_id, guid, mime_type, filename, file_size, slug, alt, created_at FROM uploads WHERE site_id = ? AND slug = ? LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
type SelectUploadBySiteIDAndSlugParams struct {
|
||||||
|
SiteID int64
|
||||||
|
Slug string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SelectUploadBySiteIDAndSlug(ctx context.Context, arg SelectUploadBySiteIDAndSlugParams) (Upload, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, selectUploadBySiteIDAndSlug, arg.SiteID, arg.Slug)
|
||||||
|
var i Upload
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SiteID,
|
||||||
|
&i.Guid,
|
||||||
|
&i.MimeType,
|
||||||
|
&i.Filename,
|
||||||
|
&i.FileSize,
|
||||||
|
&i.Slug,
|
||||||
|
&i.Alt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectUploadsOfSite = `-- name: SelectUploadsOfSite :many
|
||||||
|
SELECT id, site_id, guid, mime_type, filename, file_size, slug, alt, created_at FROM uploads WHERE site_id = ? ORDER BY created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) SelectUploadsOfSite(ctx context.Context, siteID int64) ([]Upload, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, selectUploadsOfSite, siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Upload
|
||||||
|
for rows.Next() {
|
||||||
|
var i Upload
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SiteID,
|
||||||
|
&i.Guid,
|
||||||
|
&i.MimeType,
|
||||||
|
&i.Filename,
|
||||||
|
&i.FileSize,
|
||||||
|
&i.Slug,
|
||||||
|
&i.Alt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUpload = `-- name: UpdateUpload :exec
|
||||||
|
UPDATE uploads SET alt = ? WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUploadParams struct {
|
||||||
|
Alt string
|
||||||
|
ID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUpload(ctx context.Context, arg UpdateUploadParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateUpload, arg.Alt, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
122
providers/db/uploads.go
Normal file
122
providers/db/uploads.go
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
"lmika.dev/lmika/weiro/providers/db/gen/sqlgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *Provider) SelectUploadByID(ctx context.Context, id int64) (models.Upload, error) {
|
||||||
|
row, err := db.queries.SelectUploadByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return models.Upload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbUploadToUpload(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Provider) SelectUploadsOfSite(ctx context.Context, siteID int64) ([]models.Upload, error) {
|
||||||
|
rows, err := db.queries.SelectUploadsOfSite(ctx, siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uploads := make([]models.Upload, len(rows))
|
||||||
|
for i, row := range rows {
|
||||||
|
uploads[i] = dbUploadToUpload(row)
|
||||||
|
}
|
||||||
|
return uploads, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Provider) SelectUploadBySiteIDAndSlug(ctx context.Context, siteID int64, slug string) (models.Upload, error) {
|
||||||
|
row, err := db.queries.SelectUploadBySiteIDAndSlug(ctx, sqlgen.SelectUploadBySiteIDAndSlugParams{
|
||||||
|
SiteID: siteID,
|
||||||
|
Slug: slug,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return models.Upload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbUploadToUpload(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Provider) SaveUpload(ctx context.Context, upload *models.Upload) error {
|
||||||
|
if upload.ID == 0 {
|
||||||
|
if err := db.queries.InsertUpload(ctx, sqlgen.InsertUploadParams{
|
||||||
|
SiteID: upload.SiteID,
|
||||||
|
Guid: upload.GUID,
|
||||||
|
MimeType: upload.MIMEType,
|
||||||
|
Filename: upload.Filename,
|
||||||
|
FileSize: upload.FileSize,
|
||||||
|
Slug: upload.Slug,
|
||||||
|
Alt: upload.Alt,
|
||||||
|
CreatedAt: upload.CreatedAt.Unix(),
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.queries.UpdateUpload(ctx, sqlgen.UpdateUploadParams{
|
||||||
|
Alt: upload.Alt,
|
||||||
|
ID: upload.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Provider) DeleteUpload(ctx context.Context, id int64) error {
|
||||||
|
return db.queries.DeleteUpload(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Provider) SelectPendingUploadByGUID(ctx context.Context, guid string) (models.PendingUpload, error) {
|
||||||
|
row, err := db.queries.SelectPendingUploadByGUID(ctx, guid)
|
||||||
|
if err != nil {
|
||||||
|
return models.PendingUpload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbPendingUploadToPendingUpload(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Provider) SavePendingUpload(ctx context.Context, pending *models.PendingUpload) error {
|
||||||
|
_, err := db.queries.InsertPendingUpload(ctx, sqlgen.InsertPendingUploadParams{
|
||||||
|
SiteID: pending.SiteID,
|
||||||
|
Guid: pending.GUID,
|
||||||
|
UserID: pending.UserID,
|
||||||
|
Filename: pending.Filename,
|
||||||
|
FileSize: pending.FileSize,
|
||||||
|
MimeType: pending.MIMEType,
|
||||||
|
UploadStartedAt: pending.UploadStarted.Unix(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Provider) DeletePendingUpload(ctx context.Context, guid string) error {
|
||||||
|
return db.queries.DeletePendingUpload(ctx, guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbUploadToUpload(row sqlgen.Upload) models.Upload {
|
||||||
|
return models.Upload{
|
||||||
|
ID: row.ID,
|
||||||
|
SiteID: row.SiteID,
|
||||||
|
GUID: row.Guid,
|
||||||
|
MIMEType: row.MimeType,
|
||||||
|
FileSize: row.FileSize,
|
||||||
|
Filename: row.Filename,
|
||||||
|
Slug: row.Slug,
|
||||||
|
Alt: row.Alt,
|
||||||
|
CreatedAt: time.Unix(row.CreatedAt, 0).UTC(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbPendingUploadToPendingUpload(row sqlgen.PendingUpload) models.PendingUpload {
|
||||||
|
return models.PendingUpload{
|
||||||
|
GUID: row.Guid,
|
||||||
|
SiteID: row.SiteID,
|
||||||
|
UserID: row.UserID,
|
||||||
|
FileSize: row.FileSize,
|
||||||
|
Filename: row.Filename,
|
||||||
|
MIMEType: row.MimeType,
|
||||||
|
UploadStarted: time.Unix(row.UploadStartedAt, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
27
providers/markdown/htmltransforms.go
Normal file
27
providers/markdown/htmltransforms.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
type htmlTransform func(ctx context.Context, dom *goquery.Document) error
|
||||||
|
|
||||||
|
func applyTransforms(ctx context.Context, inHTML string, transforms []htmlTransform) string {
|
||||||
|
dom, err := goquery.NewDocumentFromReader(strings.NewReader(inHTML))
|
||||||
|
if err != nil {
|
||||||
|
return inHTML
|
||||||
|
}
|
||||||
|
for _, transform := range transforms {
|
||||||
|
if err := transform(ctx, dom); err != nil {
|
||||||
|
return inHTML
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res, err := dom.Html()
|
||||||
|
if err != nil {
|
||||||
|
return inHTML
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
86
providers/markdown/renderer.go
Normal file
86
providers/markdown/renderer.go
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/extension"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
gm_html "github.com/yuin/goldmark/renderer/html"
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Renderer struct {
|
||||||
|
mdParser goldmark.Markdown
|
||||||
|
bmPolicy *bluemonday.Policy
|
||||||
|
htmlTransforms []htmlTransform
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRendererForUI() *Renderer {
|
||||||
|
mdParser := goldmark.New(
|
||||||
|
goldmark.WithExtensions(extension.GFM),
|
||||||
|
goldmark.WithRendererOptions(
|
||||||
|
gm_html.WithUnsafe(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
bmPolicy := bluemonday.UGCPolicy()
|
||||||
|
|
||||||
|
return &Renderer{
|
||||||
|
mdParser: mdParser,
|
||||||
|
bmPolicy: bmPolicy,
|
||||||
|
htmlTransforms: []htmlTransform{
|
||||||
|
rewriteImgSrc(func(ctx context.Context, src string) string {
|
||||||
|
if strings.HasPrefix(src, "/uploads/") {
|
||||||
|
if site, ok := models.GetSite(ctx); ok {
|
||||||
|
return fmt.Sprintf("/sites/%v/uploads/slug/%v", site.ID, strings.TrimPrefix(src, "/uploads/"))
|
||||||
|
}
|
||||||
|
return src
|
||||||
|
}
|
||||||
|
return src
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRendererForSite() *Renderer {
|
||||||
|
mdParser := goldmark.New(
|
||||||
|
goldmark.WithExtensions(extension.GFM),
|
||||||
|
goldmark.WithParserOptions(
|
||||||
|
parser.WithAutoHeadingID(),
|
||||||
|
),
|
||||||
|
goldmark.WithRendererOptions(
|
||||||
|
gm_html.WithUnsafe(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &Renderer{
|
||||||
|
mdParser: mdParser,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) Render(ctx context.Context, in string) (string, error) {
|
||||||
|
var sb strings.Builder
|
||||||
|
if err := r.mdParser.Convert([]byte(in), &sb); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
outHTML := applyTransforms(ctx, sb.String(), r.htmlTransforms)
|
||||||
|
|
||||||
|
if r.bmPolicy != nil {
|
||||||
|
return r.bmPolicy.Sanitize(outHTML), nil
|
||||||
|
}
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) RenderTo(ctx context.Context, w io.Writer, in string) error {
|
||||||
|
s, err := r.Render(ctx, in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write([]byte(s))
|
||||||
|
return err
|
||||||
|
}
|
||||||
21
providers/markdown/uiexts.go
Normal file
21
providers/markdown/uiexts.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func rewriteImgSrc(transform func(ctx context.Context, in string) string) htmlTransform {
|
||||||
|
return func(ctx context.Context, dom *goquery.Document) error {
|
||||||
|
dom.Find("img").Each(func(i int, s *goquery.Selection) {
|
||||||
|
s.SetAttr("src", transform(ctx, s.AttrOr("src", "")))
|
||||||
|
})
|
||||||
|
dom.Find("img").Each(func(i int, s *goquery.Selection) {
|
||||||
|
s.AddClass("img-fluid")
|
||||||
|
})
|
||||||
|
//SetAttr("style", "max-width: 200px;max-height: 200px;height: auto;")
|
||||||
|
//log.Printf("Rewritten image src: %s", s.AttrOr("style", ""))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,26 +2,24 @@ package sitebuilder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yuin/goldmark"
|
"golang.org/x/sync/errgroup"
|
||||||
"github.com/yuin/goldmark/extension"
|
|
||||||
"github.com/yuin/goldmark/parser"
|
|
||||||
"github.com/yuin/goldmark/renderer/html"
|
|
||||||
"lmika.dev/lmika/weiro/models"
|
"lmika.dev/lmika/weiro/models"
|
||||||
"lmika.dev/lmika/weiro/models/pubmodel"
|
"lmika.dev/lmika/weiro/models/pubmodel"
|
||||||
|
"lmika.dev/lmika/weiro/providers/markdown"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Builder struct {
|
type Builder struct {
|
||||||
site pubmodel.Site
|
site pubmodel.Site
|
||||||
gmMarkdown goldmark.Markdown
|
mdRenderer *markdown.Renderer
|
||||||
opts Options
|
opts Options
|
||||||
tmpls *template.Template
|
tmpls *template.Template
|
||||||
}
|
}
|
||||||
|
|
@ -38,15 +36,7 @@ func New(site pubmodel.Site, opts Options) (*Builder, error) {
|
||||||
site: site,
|
site: site,
|
||||||
opts: opts,
|
opts: opts,
|
||||||
tmpls: tmpls,
|
tmpls: tmpls,
|
||||||
gmMarkdown: goldmark.New(
|
mdRenderer: markdown.NewRendererForSite(),
|
||||||
goldmark.WithExtensions(extension.GFM),
|
|
||||||
goldmark.WithParserOptions(
|
|
||||||
parser.WithAutoHeadingID(),
|
|
||||||
),
|
|
||||||
goldmark.WithRendererOptions(
|
|
||||||
html.WithUnsafe(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,15 +47,34 @@ func (b *Builder) BuildSite(outDir string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eg := errgroup.Group{}
|
||||||
|
|
||||||
|
eg.Go(func() error {
|
||||||
for _, post := range b.site.Posts {
|
for _, post := range b.site.Posts {
|
||||||
if err := b.writePost(buildCtx, post); err != nil {
|
if err := b.writePost(buildCtx, post); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
eg.Go(func() error {
|
||||||
if err := b.renderPostList(buildCtx, b.site.Posts); err != nil {
|
if err := b.renderPostList(buildCtx, b.site.Posts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Copy uploads
|
||||||
|
eg.Go(func() error {
|
||||||
|
if err := b.writeUploads(buildCtx, b.site.Uploads); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err := eg.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +111,7 @@ func (b *Builder) renderPost(post *models.Post) (postSingleData, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var md bytes.Buffer
|
var md bytes.Buffer
|
||||||
if err := b.gmMarkdown.Convert([]byte(post.Body), &md); err != nil {
|
if err := b.mdRenderer.RenderTo(context.Background(), &md, post.Body); err != nil {
|
||||||
return postSingleData{}, fmt.Errorf("failed to write post %s: %w", post.Slug, err)
|
return postSingleData{}, fmt.Errorf("failed to write post %s: %w", post.Slug, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,7 +139,6 @@ func (b *Builder) createAtPath(ctx buildContext, path string, fn func(f io.Write
|
||||||
if filepath.Ext(outFile) == "" {
|
if filepath.Ext(outFile) == "" {
|
||||||
outFile = filepath.Join(outFile, "index.html")
|
outFile = filepath.Join(outFile, "index.html")
|
||||||
}
|
}
|
||||||
log.Printf("Writing %s\n", outFile)
|
|
||||||
|
|
||||||
// Render it within the template
|
// Render it within the template
|
||||||
if err := os.MkdirAll(filepath.Dir(outFile), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(outFile), 0755); err != nil {
|
||||||
|
|
@ -158,6 +166,37 @@ func (b *Builder) renderTemplate(w io.Writer, name string, data interface{}) err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Builder) writeUploads(ctx buildContext, uploads []models.Upload) error {
|
||||||
|
for _, u := range uploads {
|
||||||
|
fullPath := filepath.Join(ctx.outDir, "uploads", u.Slug)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := func() error {
|
||||||
|
r, err := b.site.OpenUpload(u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
w, err := os.Create(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(w, r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type buildContext struct {
|
type buildContext struct {
|
||||||
outDir string
|
outDir string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
58
providers/uploadfiles/exif.go
Normal file
58
providers/uploadfiles/exif.go
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package uploadfiles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
applicationOctetStream = "application/octet-stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
var supportedRencodableImageTypes = map[string]bool{
|
||||||
|
"image/jpeg": true,
|
||||||
|
"image/png": true,
|
||||||
|
applicationOctetStream: true,
|
||||||
|
}
|
||||||
|
var supportedReencoableExtensions = map[string]bool{
|
||||||
|
".jpg": true,
|
||||||
|
".jpeg": true,
|
||||||
|
".png": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) StripeEXIFData(site models.Site, up models.Upload) error {
|
||||||
|
uploadFilename := p.uploadFileName(site, up)
|
||||||
|
|
||||||
|
if !supportedRencodableImageTypes[up.MIMEType] {
|
||||||
|
return errors.New("unsupported image format: " + up.MIMEType)
|
||||||
|
}
|
||||||
|
if up.MIMEType == applicationOctetStream && !supportedReencoableExtensions[filepath.Ext(uploadFilename)] {
|
||||||
|
return errors.New("unsupported image format")
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := imaging.Open(uploadFilename)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to open image file")
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpName := strings.TrimSuffix(uploadFilename, filepath.Ext(uploadFilename)) + ".tmp." + filepath.Ext(uploadFilename)
|
||||||
|
if err := imaging.Save(img, tmpName); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to save image file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(uploadFilename); err != nil {
|
||||||
|
_ = os.Remove(tmpName)
|
||||||
|
return errors.Wrap(err, "failed to remove image file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(tmpName, uploadFilename); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to rename image file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
57
providers/uploadfiles/provider.go
Normal file
57
providers/uploadfiles/provider.go
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package uploadfiles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
baseDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(baseDir string) *Provider {
|
||||||
|
return &Provider{
|
||||||
|
baseDir: baseDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) AdoptFile(site models.Site, up models.Upload, filename string) error {
|
||||||
|
fullPath := p.uploadFileName(site, up)
|
||||||
|
baseDir := filepath.Dir(fullPath)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(baseDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(filename, fullPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) OpenUpload(site models.Site, up models.Upload) (io.ReadCloser, error) {
|
||||||
|
fullPath := p.uploadFileName(site, up)
|
||||||
|
return os.Open(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) DeleteUpload(site models.Site, up models.Upload) error {
|
||||||
|
fullPath := p.uploadFileName(site, up)
|
||||||
|
if err := os.Remove(fullPath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) UploadDir(site models.Site) string {
|
||||||
|
return filepath.Join(p.baseDir, site.GUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) uploadFileName(site models.Site, up models.Upload) string {
|
||||||
|
return filepath.Join(p.baseDir, site.GUID, up.Slug)
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,13 @@ func (s *Service) fetchPostAndSite(ctx context.Context, pid int64) (*models.Post
|
||||||
return nil, models.Site{}, models.SiteRequiredError
|
return nil, models.Site{}, models.SiteRequiredError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user, ok := models.GetUser(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, models.Site{}, models.UserRequiredError
|
||||||
|
} else if site.OwnerID != user.ID {
|
||||||
|
return nil, models.Site{}, models.PermissionError
|
||||||
|
}
|
||||||
|
|
||||||
post, err := s.db.SelectPost(ctx, pid)
|
post, err := s.db.SelectPost(ctx, pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, models.Site{}, err
|
return nil, models.Site{}, err
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package publisher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
|
@ -16,15 +17,18 @@ import (
|
||||||
"lmika.dev/lmika/weiro/providers/db"
|
"lmika.dev/lmika/weiro/providers/db"
|
||||||
"lmika.dev/lmika/weiro/providers/sitebuilder"
|
"lmika.dev/lmika/weiro/providers/sitebuilder"
|
||||||
"lmika.dev/lmika/weiro/providers/siteexporter"
|
"lmika.dev/lmika/weiro/providers/siteexporter"
|
||||||
|
"lmika.dev/lmika/weiro/providers/uploadfiles"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Publisher struct {
|
type Publisher struct {
|
||||||
db *db.Provider
|
db *db.Provider
|
||||||
|
up *uploadfiles.Provider
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(db *db.Provider) *Publisher {
|
func New(db *db.Provider, up *uploadfiles.Provider) *Publisher {
|
||||||
return &Publisher{
|
return &Publisher{
|
||||||
db: db,
|
db: db,
|
||||||
|
up: up,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,6 +44,12 @@ func (p *Publisher) Publish(ctx context.Context, site models.Site) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch all uploads of site
|
||||||
|
uploads, err := p.db.SelectUploadsOfSite(ctx, site.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
if !target.Enabled {
|
if !target.Enabled {
|
||||||
continue
|
continue
|
||||||
|
|
@ -49,6 +59,10 @@ func (p *Publisher) Publish(ctx context.Context, site models.Site) error {
|
||||||
Site: site,
|
Site: site,
|
||||||
Posts: posts,
|
Posts: posts,
|
||||||
BaseURL: target.BaseURL,
|
BaseURL: target.BaseURL,
|
||||||
|
Uploads: uploads,
|
||||||
|
OpenUpload: func(u models.Upload) (io.ReadCloser, error) {
|
||||||
|
return p.up.OpenUpload(site, u)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.publishSite(ctx, pubSite, target); err != nil {
|
if err := p.publishSite(ctx, pubSite, target); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ import (
|
||||||
|
|
||||||
"lmika.dev/lmika/weiro/config"
|
"lmika.dev/lmika/weiro/config"
|
||||||
"lmika.dev/lmika/weiro/providers/db"
|
"lmika.dev/lmika/weiro/providers/db"
|
||||||
|
"lmika.dev/lmika/weiro/providers/uploadfiles"
|
||||||
"lmika.dev/lmika/weiro/services/auth"
|
"lmika.dev/lmika/weiro/services/auth"
|
||||||
"lmika.dev/lmika/weiro/services/posts"
|
"lmika.dev/lmika/weiro/services/posts"
|
||||||
"lmika.dev/lmika/weiro/services/publisher"
|
"lmika.dev/lmika/weiro/services/publisher"
|
||||||
"lmika.dev/lmika/weiro/services/sites"
|
"lmika.dev/lmika/weiro/services/sites"
|
||||||
|
"lmika.dev/lmika/weiro/services/uploads"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Services struct {
|
type Services struct {
|
||||||
|
|
@ -18,6 +20,7 @@ type Services struct {
|
||||||
PublisherQueue *publisher.Queue
|
PublisherQueue *publisher.Queue
|
||||||
Posts *posts.Service
|
Posts *posts.Service
|
||||||
Sites *sites.Service
|
Sites *sites.Service
|
||||||
|
Uploads *uploads.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg config.Config) (*Services, error) {
|
func New(cfg config.Config) (*Services, error) {
|
||||||
|
|
@ -26,11 +29,14 @@ func New(cfg config.Config) (*Services, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ufp := uploadfiles.New(filepath.Join(cfg.DataDir, "uploads"))
|
||||||
|
|
||||||
authSvc := auth.New(dbp)
|
authSvc := auth.New(dbp)
|
||||||
publisherSvc := publisher.New(dbp)
|
publisherSvc := publisher.New(dbp, ufp)
|
||||||
publisherQueue := publisher.NewQueue(publisherSvc)
|
publisherQueue := publisher.NewQueue(publisherSvc)
|
||||||
postService := posts.New(dbp, publisherQueue)
|
postService := posts.New(dbp, publisherQueue)
|
||||||
siteService := sites.New(dbp)
|
siteService := sites.New(dbp)
|
||||||
|
uploadService := uploads.New(dbp, ufp, filepath.Join(cfg.ScratchDir, "uploads", "pending"))
|
||||||
|
|
||||||
return &Services{
|
return &Services{
|
||||||
DB: dbp,
|
DB: dbp,
|
||||||
|
|
@ -39,6 +45,7 @@ func New(cfg config.Config) (*Services, error) {
|
||||||
PublisherQueue: publisherQueue,
|
PublisherQueue: publisherQueue,
|
||||||
Posts: postService,
|
Posts: postService,
|
||||||
Sites: siteService,
|
Sites: siteService,
|
||||||
|
Uploads: uploadService,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
135
services/uploads/manage.go
Normal file
135
services/uploads/manage.go
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
package uploads
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
uploadCopyTemplate = template.Must(template.New("upload-copy").Parse(`<img src="/uploads/{{.Slug}}" alt="{{.Alt}}">`))
|
||||||
|
)
|
||||||
|
|
||||||
|
type UploadWithURL struct {
|
||||||
|
Upload models.Upload
|
||||||
|
CopyTemplate string
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) FetchUpload(ctx context.Context, uploadID int64) (res UploadWithURL, _ error) {
|
||||||
|
site, _, err := s.fetchSiteAndUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return UploadWithURL{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
upload, err := s.db.SelectUploadByID(ctx, uploadID)
|
||||||
|
if err != nil {
|
||||||
|
return UploadWithURL{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return UploadWithURL{
|
||||||
|
Upload: upload,
|
||||||
|
CopyTemplate: s.renderCopyTemplate(upload),
|
||||||
|
URL: fmt.Sprintf("/sites/%v/uploads/%v/raw", site.ID, upload.ID),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteUpload(ctx context.Context, uploadID int64) error {
|
||||||
|
site, _, err := s.fetchSiteAndUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
upload, err := s.db.SelectUploadByID(ctx, uploadID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.up.DeleteUpload(site, upload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.db.DeleteUpload(ctx, uploadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) renderCopyTemplate(upload models.Upload) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
if err := uploadCopyTemplate.Execute(&sb, upload); err != nil {
|
||||||
|
log.Printf("error rendering upload copy template: %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListUploads(ctx context.Context) (res []UploadWithURL, _ error) {
|
||||||
|
site, _, err := s.fetchSiteAndUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uploads, err := s.db.SelectUploadsOfSite(ctx, site.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res = make([]UploadWithURL, len(uploads))
|
||||||
|
for i, upload := range uploads {
|
||||||
|
res[i] = UploadWithURL{
|
||||||
|
Upload: upload,
|
||||||
|
CopyTemplate: s.renderCopyTemplate(upload),
|
||||||
|
URL: fmt.Sprintf("/sites/%v/uploads/%v/raw", site.ID, upload.ID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) OpenUpload(ctx context.Context, id int64) (models.Upload, func() (io.ReadCloser, error), error) {
|
||||||
|
site, _, err := s.fetchSiteAndUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return models.Upload{}, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
upload, err := s.db.SelectUploadByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return models.Upload{}, nil, err
|
||||||
|
} else if upload.SiteID != site.ID {
|
||||||
|
return models.Upload{}, nil, models.NotFoundError
|
||||||
|
}
|
||||||
|
|
||||||
|
return upload, func() (io.ReadCloser, error) {
|
||||||
|
rw, err := s.up.OpenUpload(site, upload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rw, nil
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) OpenUploadFromSlug(ctx context.Context, slug string) (models.Upload, func() (io.ReadCloser, error), error) {
|
||||||
|
site, _, err := s.fetchSiteAndUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return models.Upload{}, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
upload, err := s.db.SelectUploadBySiteIDAndSlug(ctx, site.ID, slug)
|
||||||
|
if err != nil {
|
||||||
|
return models.Upload{}, nil, err
|
||||||
|
} else if upload.SiteID != site.ID {
|
||||||
|
return models.Upload{}, nil, models.NotFoundError
|
||||||
|
}
|
||||||
|
|
||||||
|
return upload, func() (io.ReadCloser, error) {
|
||||||
|
rw, err := s.up.OpenUpload(site, upload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rw, nil
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
167
services/uploads/pending.go
Normal file
167
services/uploads/pending.go
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
package uploads
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"emperror.dev/errors"
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NewPendingRequest struct {
|
||||||
|
FileSize int64 `json:"size"`
|
||||||
|
Filename string `json:"name"`
|
||||||
|
MIMEType string `json:"mime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) NewPending(ctx context.Context, req NewPendingRequest) (models.PendingUpload, error) {
|
||||||
|
site, user, err := s.fetchSiteAndUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return models.PendingUpload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pending := models.PendingUpload{
|
||||||
|
GUID: models.NewNanoID(),
|
||||||
|
SiteID: site.ID,
|
||||||
|
UserID: user.ID,
|
||||||
|
FileSize: req.FileSize,
|
||||||
|
Filename: req.Filename,
|
||||||
|
MIMEType: req.MIMEType,
|
||||||
|
UploadStarted: time.Now(),
|
||||||
|
}
|
||||||
|
if err := s.db.SavePendingUpload(ctx, &pending); err != nil {
|
||||||
|
return models.PendingUpload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(s.pendingDir, 0755); err != nil {
|
||||||
|
return models.PendingUpload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingDataFile, err := os.Create(filepath.Join(s.pendingDir, pending.GUID+".upload"))
|
||||||
|
if err != nil {
|
||||||
|
return models.PendingUpload{}, err
|
||||||
|
}
|
||||||
|
return pending, pendingDataFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) WriteToPending(ctx context.Context, pendingGUID string, data []byte) error {
|
||||||
|
site, user, err := s.fetchSiteAndUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pu, err := s.db.SelectPendingUploadByGUID(ctx, pendingGUID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if pu.SiteID != site.ID || pu.UserID != user.ID {
|
||||||
|
return errors.New("invalid pending upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingDataFilename := filepath.Join(s.pendingDir, pu.GUID+".upload")
|
||||||
|
if _, err := os.Stat(pendingDataFilename); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingDataFile, err := os.OpenFile(pendingDataFilename, os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer pendingDataFile.Close()
|
||||||
|
|
||||||
|
pendingDataFile.Seek(0, io.SeekEnd)
|
||||||
|
if _, err := pendingDataFile.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) FinalizePending(ctx context.Context, pendingGUID string, expectedHash string) error {
|
||||||
|
site, user, err := s.fetchSiteAndUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pu, err := s.db.SelectPendingUploadByGUID(ctx, pendingGUID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if pu.SiteID != site.ID || pu.UserID != user.ID {
|
||||||
|
return errors.New("invalid pending upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingDataFilename := filepath.Join(s.pendingDir, pu.GUID+".upload")
|
||||||
|
fileSize, err := s.verifyPendingUpload(pendingDataFilename, expectedHash)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newUploadGUID := models.NewNanoID()
|
||||||
|
newTime := time.Now().UTC()
|
||||||
|
newSlug := filepath.Join(
|
||||||
|
fmt.Sprintf("%04d", newTime.Year()),
|
||||||
|
fmt.Sprintf("%02d", newTime.Month()),
|
||||||
|
newUploadGUID+filepath.Ext(pu.Filename),
|
||||||
|
)
|
||||||
|
|
||||||
|
newUpload := models.Upload{
|
||||||
|
SiteID: site.ID,
|
||||||
|
GUID: models.NewNanoID(),
|
||||||
|
FileSize: fileSize,
|
||||||
|
MIMEType: pu.MIMEType,
|
||||||
|
Filename: pu.Filename,
|
||||||
|
CreatedAt: newTime,
|
||||||
|
Slug: newSlug,
|
||||||
|
}
|
||||||
|
if err := s.db.SaveUpload(ctx, &newUpload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.up.AdoptFile(site, newUpload, pendingDataFilename); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.db.DeletePendingUpload(ctx, pendingGUID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.up.StripeEXIFData(site, newUpload); err != nil {
|
||||||
|
log.Printf("warn: failed to extract exif data from %s: %v\n", newUpload.Slug, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) verifyPendingUpload(pendingDataFilename string, expectedHash string) (fileSize int64, _ error) {
|
||||||
|
expectedHashBytes, err := hex.DecodeString(expectedHash)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := os.Stat(pendingDataFilename)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingDataFile, err := os.Open(pendingDataFilename)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer pendingDataFile.Close()
|
||||||
|
|
||||||
|
shaSum := sha256.New()
|
||||||
|
if _, err := io.Copy(shaSum, pendingDataFile); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(shaSum.Sum(nil), expectedHashBytes) {
|
||||||
|
return 0, errors.New("hash mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats.Size(), nil
|
||||||
|
}
|
||||||
41
services/uploads/services.go
Normal file
41
services/uploads/services.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
package uploads
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
"lmika.dev/lmika/weiro/providers/db"
|
||||||
|
"lmika.dev/lmika/weiro/providers/uploadfiles"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
db *db.Provider
|
||||||
|
up *uploadfiles.Provider
|
||||||
|
pendingDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *db.Provider, up *uploadfiles.Provider, pendingDir string) *Service {
|
||||||
|
return &Service{
|
||||||
|
db: db,
|
||||||
|
up: up,
|
||||||
|
pendingDir: pendingDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) fetchSiteAndUser(ctx context.Context) (models.Site, models.User, error) {
|
||||||
|
user, ok := models.GetUser(ctx)
|
||||||
|
if !ok {
|
||||||
|
return models.Site{}, models.User{}, models.UserRequiredError
|
||||||
|
}
|
||||||
|
|
||||||
|
site, ok := models.GetSite(ctx)
|
||||||
|
if !ok {
|
||||||
|
return models.Site{}, models.User{}, models.SiteRequiredError
|
||||||
|
}
|
||||||
|
|
||||||
|
if site.OwnerID != user.ID {
|
||||||
|
return models.Site{}, models.User{}, models.PermissionError
|
||||||
|
}
|
||||||
|
|
||||||
|
return site, user, nil
|
||||||
|
}
|
||||||
17
sql/queries/pending_uploads.sql
Normal file
17
sql/queries/pending_uploads.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- name: SelectPendingUploadByGUID :one
|
||||||
|
SELECT * FROM pending_uploads WHERE guid = ? LIMIT 1;
|
||||||
|
|
||||||
|
-- name: InsertPendingUpload :one
|
||||||
|
INSERT INTO pending_uploads (
|
||||||
|
site_id,
|
||||||
|
guid,
|
||||||
|
user_id,
|
||||||
|
filename,
|
||||||
|
file_size,
|
||||||
|
mime_type,
|
||||||
|
upload_started_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
RETURNING id;
|
||||||
|
|
||||||
|
-- name: DeletePendingUpload :exec
|
||||||
|
DELETE FROM pending_uploads WHERE guid = ?;
|
||||||
27
sql/queries/uploads.sql
Normal file
27
sql/queries/uploads.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
-- name: SelectUploadsOfSite :many
|
||||||
|
SELECT * FROM uploads WHERE site_id = ? ORDER BY created_at DESC;
|
||||||
|
|
||||||
|
-- name: SelectUploadByID :one
|
||||||
|
SELECT * FROM uploads WHERE id = ? LIMIT 1;
|
||||||
|
|
||||||
|
-- name: SelectUploadBySiteIDAndSlug :one
|
||||||
|
SELECT * FROM uploads WHERE site_id = ? AND slug = ? LIMIT 1;
|
||||||
|
|
||||||
|
-- name: InsertUpload :exec
|
||||||
|
INSERT INTO uploads (
|
||||||
|
site_id,
|
||||||
|
guid,
|
||||||
|
mime_type,
|
||||||
|
filename,
|
||||||
|
file_size,
|
||||||
|
slug,
|
||||||
|
alt,
|
||||||
|
created_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
RETURNING id;
|
||||||
|
|
||||||
|
-- name: UpdateUpload :exec
|
||||||
|
UPDATE uploads SET alt = ? WHERE id = ?;
|
||||||
|
|
||||||
|
-- name: DeleteUpload :exec
|
||||||
|
DELETE FROM uploads WHERE id = ?;
|
||||||
29
sql/schema/02_upload.up.sql
Normal file
29
sql/schema/02_upload.up.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
CREATE TABLE uploads (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
site_id INT NOT NULL,
|
||||||
|
guid TEXT NOT NULL,
|
||||||
|
mime_type TEXT NOT NULL,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
file_size INT NOT NULL,
|
||||||
|
slug TEXT NOT NULL,
|
||||||
|
alt TEXT NOT NULL,
|
||||||
|
created_at INT NOT NULL,
|
||||||
|
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_uploads_site ON uploads (site_id);
|
||||||
|
CREATE UNIQUE INDEX idx_uploads_guid ON uploads (guid);
|
||||||
|
CREATE UNIQUE INDEX idx_uploads_site_slug ON uploads (site_id, slug);
|
||||||
|
|
||||||
|
CREATE TABLE pending_uploads (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
site_id INT NOT NULL,
|
||||||
|
guid TEXT NOT NULL,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
file_size INT NOT NULL,
|
||||||
|
mime_type TEXT NOT NULL,
|
||||||
|
upload_started_at INT NOT NULL,
|
||||||
|
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX idx_pending_uploads_guid ON pending_uploads (guid);
|
||||||
|
|
@ -10,6 +10,9 @@
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/posts">Posts</a>
|
<a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/posts">Posts</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/uploads">Uploads</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form class="d-flex align-items-center" role="search">
|
<form class="d-flex align-items-center" role="search">
|
||||||
<!--
|
<!--
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,11 @@
|
||||||
<div data-controller="postlist"
|
<div data-controller="postlist"
|
||||||
data-postlist-site-id-value="{{ $p.SiteID }}"
|
data-postlist-site-id-value="{{ $p.SiteID }}"
|
||||||
data-postlist-post-id-value="{{ $p.ID }}"
|
data-postlist-post-id-value="{{ $p.ID }}"
|
||||||
data-postlist-nano-summary-value="{{ $p.NanoSummary }}">
|
data-postlist-nano-summary-value="{{ $p.NanoSummary }}"
|
||||||
<div class="my-4">
|
class="postlist">
|
||||||
|
<div class="my-4 post">
|
||||||
{{ if $p.Title }}<h4 class="mb-3">{{ $p.Title }}</h4>{{ end }}
|
{{ if $p.Title }}<h4 class="mb-3">{{ $p.Title }}</h4>{{ end }}
|
||||||
{{ $p.Body | markdown }}
|
{{ markdown $p.Body $.site }}
|
||||||
|
|
||||||
<div class="mb-3 d-flex align-items-center">
|
<div class="mb-3 d-flex align-items-center">
|
||||||
{{ if eq .State 1 }}
|
{{ if eq .State 1 }}
|
||||||
|
|
|
||||||
24
views/uploads/index.html
Normal file
24
views/uploads/index.html
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<main class="container">
|
||||||
|
<div class="my-4 d-flex justify-left align-items-baseline"
|
||||||
|
data-controller="upload" data-upload-site-id-value="{{ .site.ID }}">
|
||||||
|
<button class="btn btn-success" data-upload-target="uploadBtn" data-action="upload#upload">Upload</button>
|
||||||
|
<div class="progress upload-progressbar ms-3 d-none" data-upload-target="progressbar"
|
||||||
|
role="progressbar" aria-label="Basic example" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
||||||
|
<div class="progress-bar" style="width: 0" data-upload-target="progressbarProgress"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ range .uploads }}
|
||||||
|
<div class="row row-cols-5">
|
||||||
|
{{ range . }}
|
||||||
|
<div class="col">
|
||||||
|
<a href="/sites/{{ $.site.ID }}/uploads/{{ .Upload.ID }}">
|
||||||
|
<div class="ratio ratio-1x1 m-2">
|
||||||
|
<img src="{{ .URL }}" alt="{{ .Upload.Alt }}" class="img-fluid object-fit-contain">
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</main>
|
||||||
14
views/uploads/show.html
Normal file
14
views/uploads/show.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<main class="container show-upload">
|
||||||
|
<div class="my-4 d-flex justify-content-between align-items-baseline"
|
||||||
|
data-controller="show-upload"
|
||||||
|
data-show-upload-copy-snippet-value="{{ .upload.CopyTemplate }}"
|
||||||
|
data-show-upload-site-id-value="{{ .upload.Upload.SiteID }}"
|
||||||
|
data-show-upload-upload-id-value="{{ .upload.Upload.ID }}">
|
||||||
|
<button class="btn btn-outline-dark" data-action="show-upload#copy">Copy HTML</button>
|
||||||
|
<button class="btn btn-danger" data-action="show-upload#delete">Delete</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="{{ .upload.URL }}" alt="{{ .upload.Upload.Alt }}" class="img-fluid">
|
||||||
|
</figure>
|
||||||
|
</main>
|
||||||
Loading…
Reference in a new issue