More changes to uploads:

- Have got upload images appearing in the post list
- Allowed for deleting uploads
- Allowed for seeing the upload progress
- Fixed the setting of upload properties like the MIME type
- Removed the stripe exif logic with just re-encoding PNGs and JPEGs by loading them and saving them
This commit is contained in:
Leon Mika 2026-03-04 22:33:39 +11:00
parent d0cebe6564
commit 199ff9feb9
21 changed files with 471 additions and 65 deletions

View file

@ -25,3 +25,19 @@ $container-max-widths: (
.post-form textarea {
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;
}

View file

@ -4,6 +4,8 @@ import {showToast} from "../services/toast";
export default class ShowUploadController extends Controller {
static values = {
copySnippet: String,
siteId: Number,
uploadId: Number,
};
async copy(ev) {
@ -16,4 +18,24 @@ export default class ShowUploadController extends Controller {
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'
}
})
}
}

View file

@ -1,6 +1,12 @@
import { Controller } from "@hotwired/stimulus"
export default class UploadController extends Controller {
static targets = [
'uploadBtn',
'progressbar',
'progressbarProgress',
];
static values = {
siteId: Number,
};
@ -30,13 +36,17 @@ export default class UploadController extends Controller {
}
async _doUploads(files) {
for (let file of files) {
await this._doUpload(file);
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) {
async _doUpload(file, thisFileIndex, nFiles) {
console.log(`Uploading ${file.name}: new pending`);
// Prepare upload of file supplying size and mime-type
@ -60,12 +70,11 @@ export default class UploadController extends Controller {
let chunk = file.slice(offset, offset + chunkSize);
console.log(`Uploading ${file.name}: uploading part`);
await fetch(`/sites/${this.siteIdValue}/uploads/pending/${newPending.guid}`, {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream'
},
body: chunk
await this._uploadChunk(`/sites/${this.siteIdValue}/uploads/pending/${newPending.guid}`, chunk, {
chunkOffset: offset,
totalSize: file.size,
thisFileIndex,
nFiles,
});
offset += chunkSize;
@ -88,6 +97,44 @@ export default class UploadController extends Controller {
});
}
_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);

View file

@ -6,7 +6,6 @@ import (
"html/template"
"log"
"path/filepath"
"strings"
"time"
"github.com/gofiber/fiber/v3"
@ -17,11 +16,11 @@ import (
fiber_html "github.com/gofiber/template/html/v3"
"github.com/gofiber/utils/v2"
"github.com/spf13/cobra"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"lmika.dev/lmika/weiro/config"
"lmika.dev/lmika/weiro/handlers"
"lmika.dev/lmika/weiro/handlers/middleware"
"lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/providers/markdown"
"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.Funcmap["sub"] = func(x, y int) int { return x - y }
fiberTemplate.Funcmap["markdown"] = func() func(s string) template.HTML {
mdParser := goldmark.New(
goldmark.WithExtensions(extension.GFM),
)
return func(s string) template.HTML {
var sb strings.Builder
if err := mdParser.Convert([]byte(s), &sb); err != nil {
fiberTemplate.Funcmap["markdown"] = func() func(s string, site models.Site) template.HTML {
mdParser := markdown.NewRendererForUI()
return func(s string, site models.Site) template.HTML {
ctx := context.Background()
if site.ID != 0 {
ctx = models.WithSite(ctx, site)
}
s, err := mdParser.Render(ctx, s)
if err != nil {
return template.HTML("Markdown error: " + html.EscapeString(err.Error()))
}
return template.HTML(sb.String())
return template.HTML(s)
}
}()
@ -124,11 +126,13 @@ Starting weiro without any arguments will start the server.
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("/first-run", ih.FirstRun)

7
go.mod
View file

@ -19,14 +19,18 @@ require (
github.com/Azure/go-autorest/logger v0.1.0 // indirect
github.com/Azure/go-autorest/tracing v0.5.0 // 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/urlesc v0.0.0-20170810143723-de5bf2ad4578 // 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/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/davecgh/go-spew v1.1.1 // 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/go-openapi/analysis v0.19.16 // indirect
github.com/go-openapi/errors v0.19.9 // indirect
@ -48,6 +52,7 @@ require (
github.com/gofiber/template/v2 v2.1.0 // indirect
github.com/gofiber/utils/v2 v2.0.2 // 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/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
@ -58,6 +63,7 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/ncruces/go-strftime v1.0.0 // indirect
github.com/netlify/open-api/v2 v2.49.1 // indirect
@ -77,6 +83,7 @@ require (
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.48.0 // 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/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect

53
go.sum
View file

@ -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/go.mod h1:WlIhYi++8FlKNJtrop1mjXYAJMzv1f43K4MqCoh0yGE=
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.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
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/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
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/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=
@ -57,6 +61,8 @@ 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/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/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=
@ -83,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/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/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.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@ -257,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.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.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -270,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.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
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/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=
@ -357,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/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -507,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-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.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/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
@ -520,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/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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -534,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.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.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/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -561,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-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.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/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/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
@ -579,6 +608,11 @@ 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-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.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/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@ -612,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.5.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/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
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-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.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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -629,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.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
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/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
@ -664,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-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.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/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=

View file

@ -2,12 +2,14 @@ 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"
)
@ -45,6 +47,23 @@ func (uh UploadsHandler) Show(c fiber.Ctx) error {
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 == "" {
@ -61,10 +80,29 @@ func (uh UploadsHandler) ShowRaw(c fiber.Ctx) error {
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 := rwFn()
rw, err := uploadReader()
if err != nil {
return
}
@ -75,7 +113,6 @@ func (uh UploadsHandler) ShowRaw(c fiber.Ctx) error {
return
}
})
}
func (uh UploadsHandler) New(c fiber.Ctx) error {

View file

@ -25,3 +25,8 @@ func GetSite(ctx context.Context) (Site, bool) {
site, ok := ctx.Value(siteKey).(Site)
return site, ok
}
func MustGetSite(ctx context.Context) Site {
site, _ := GetSite(ctx)
return site
}

View file

@ -20,6 +20,6 @@ type PendingUpload struct {
UserID int64 `json:"user_id"`
FileSize int64 `json:"file_size"`
Filename string `json:"filename"`
MIMEType string `json:"mime_type"`
MIMEType string `json:"mime_mime"`
UploadStarted time.Time `json:"upload_started"`
}

View 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
}

View 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
}

View 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
}
}

View file

@ -2,6 +2,7 @@ package sitebuilder
import (
"bytes"
"context"
"fmt"
"html/template"
"io"
@ -10,18 +11,15 @@ import (
"sort"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"golang.org/x/sync/errgroup"
"lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/models/pubmodel"
"lmika.dev/lmika/weiro/providers/markdown"
)
type Builder struct {
site pubmodel.Site
gmMarkdown goldmark.Markdown
mdRenderer *markdown.Renderer
opts Options
tmpls *template.Template
}
@ -35,18 +33,10 @@ func New(site pubmodel.Site, opts Options) (*Builder, error) {
}
return &Builder{
site: site,
opts: opts,
tmpls: tmpls,
gmMarkdown: goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
),
site: site,
opts: opts,
tmpls: tmpls,
mdRenderer: markdown.NewRendererForSite(),
}, nil
}
@ -121,7 +111,7 @@ func (b *Builder) renderPost(post *models.Post) (postSingleData, error) {
}
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)
}

View file

@ -1,31 +1,57 @@
package uploadfiles
import (
"os"
"path/filepath"
"strings"
"emperror.dev/errors"
"github.com/barasher/go-exiftool"
"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)
et, err := exiftool.NewExiftool()
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 err
return errors.Wrap(err, "failed to open image file")
}
defer et.Close()
fileInfos := et.ExtractMetadata(uploadFilename)
if len(fileInfos) == 0 {
return errors.New("no exif data found")
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")
}
fileInfo := fileInfos[0]
fileInfo.ClearAll()
fileOut := []exiftool.FileMetadata{fileInfo}
et.WriteMetadata(fileOut)
if fileOut[0].Err != nil {
return fileOut[0].Err
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

View file

@ -37,6 +37,17 @@ func (p *Provider) OpenUpload(site models.Site, up models.Upload) (io.ReadCloser
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)
}

View file

@ -39,6 +39,24 @@ func (s *Service) FetchUpload(ctx context.Context, uploadID int64) (res UploadWi
}, 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
@ -93,3 +111,25 @@ func (s *Service) OpenUpload(ctx context.Context, id int64) (models.Upload, func
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
}

View file

@ -19,7 +19,7 @@ import (
type NewPendingRequest struct {
FileSize int64 `json:"size"`
Filename string `json:"name"`
MIMEType string `json:"type"`
MIMEType string `json:"mime"`
}
func (s *Service) NewPending(ctx context.Context, req NewPendingRequest) (models.PendingUpload, error) {
@ -126,7 +126,7 @@ func (s *Service) FinalizePending(ctx context.Context, pendingGUID string, expec
if err := s.up.AdoptFile(site, newUpload, pendingDataFilename); err != nil {
return err
}
if err := s.db.DeletePendingUpload(ctx, newUpload.GUID); err != nil {
if err := s.db.DeletePendingUpload(ctx, pendingGUID); err != nil {
return err
}
if err := s.up.StripeEXIFData(site, newUpload); err != nil {

View file

@ -10,6 +10,9 @@
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/posts">Posts</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/uploads">Uploads</a>
</li>
</ul>
<form class="d-flex align-items-center" role="search">
<!--

View file

@ -18,10 +18,11 @@
<div data-controller="postlist"
data-postlist-site-id-value="{{ $p.SiteID }}"
data-postlist-post-id-value="{{ $p.ID }}"
data-postlist-nano-summary-value="{{ $p.NanoSummary }}">
<div class="my-4">
data-postlist-nano-summary-value="{{ $p.NanoSummary }}"
class="postlist">
<div class="my-4 post">
{{ 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">
{{ if eq .State 1 }}

View file

@ -1,7 +1,11 @@
<main class="container">
<div class="my-4 d-flex justify-content-between align-items-baseline"
<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-action="upload#upload">Upload</button>
<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 }}

View file

@ -1,8 +1,14 @@
<main class="container">
<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 }}">
<button class="btn" data-action="show-upload#copy">Copy HTML</button>
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>
<img src="{{ .upload.URL }}" alt="{{ .upload.Upload.Alt }}" class="img-fluid">
<figure>
<img src="{{ .upload.URL }}" alt="{{ .upload.Upload.Alt }}" class="img-fluid">
</figure>
</main>