2026-03-02 11:26:40 +00:00
|
|
|
package uploads
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
2026-03-03 11:36:24 +00:00
|
|
|
"html/template"
|
2026-03-02 11:26:40 +00:00
|
|
|
"io"
|
2026-03-03 11:36:24 +00:00
|
|
|
"log"
|
2026-03-28 10:42:35 +00:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2026-03-03 11:36:24 +00:00
|
|
|
"strings"
|
2026-03-28 10:42:35 +00:00
|
|
|
"time"
|
2026-03-02 11:26:40 +00:00
|
|
|
|
|
|
|
|
"lmika.dev/lmika/weiro/models"
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-03 11:36:24 +00:00
|
|
|
var (
|
|
|
|
|
uploadCopyTemplate = template.Must(template.New("upload-copy").Parse(`<img src="/uploads/{{.Slug}}" alt="{{.Alt}}">`))
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-02 11:26:40 +00:00
|
|
|
type UploadWithURL struct {
|
2026-03-03 11:36:24 +00:00
|
|
|
Upload models.Upload
|
|
|
|
|
CopyTemplate string
|
|
|
|
|
URL string
|
2026-03-02 11:26:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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{
|
2026-03-03 11:36:24 +00:00
|
|
|
Upload: upload,
|
|
|
|
|
CopyTemplate: s.renderCopyTemplate(upload),
|
|
|
|
|
URL: fmt.Sprintf("/sites/%v/uploads/%v/raw", site.ID, upload.ID),
|
2026-03-02 11:26:40 +00:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 11:33:39 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 11:36:24 +00:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 10:42:35 +00:00
|
|
|
func (s *Service) ReplaceUploadFile(ctx context.Context, uploadID int64, srcPath string) (models.Upload, error) {
|
|
|
|
|
site, _, err := s.fetchSiteAndUser(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return models.Upload{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
upload, err := s.db.SelectUploadByID(ctx, uploadID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return models.Upload{}, err
|
|
|
|
|
} else if upload.SiteID != site.ID {
|
|
|
|
|
return models.Upload{}, models.NotFoundError
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.up.ReplaceFile(site, upload, srcPath); err != nil {
|
|
|
|
|
return models.Upload{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stat, err := os.Stat(srcPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return models.Upload{}, err
|
|
|
|
|
}
|
|
|
|
|
upload.FileSize = stat.Size()
|
|
|
|
|
|
|
|
|
|
if err := s.db.UpdateUploadFileSize(ctx, upload.ID, upload.FileSize); err != nil {
|
|
|
|
|
return models.Upload{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return upload, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) CreateUploadFromFile(ctx context.Context, srcPath string, filename string, mimeType string) (models.Upload, error) {
|
|
|
|
|
site, _, err := s.fetchSiteAndUser(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return models.Upload{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stat, err := os.Stat(srcPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return models.Upload{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newUploadGUID := models.NewNanoID()
|
|
|
|
|
newTime := time.Now().UTC()
|
|
|
|
|
newSlug := filepath.Join(
|
|
|
|
|
fmt.Sprintf("%04d", newTime.Year()),
|
|
|
|
|
fmt.Sprintf("%02d", newTime.Month()),
|
|
|
|
|
newUploadGUID+filepath.Ext(filename),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
newUpload := models.Upload{
|
|
|
|
|
SiteID: site.ID,
|
|
|
|
|
GUID: models.NewNanoID(),
|
|
|
|
|
FileSize: stat.Size(),
|
|
|
|
|
MIMEType: mimeType,
|
|
|
|
|
Filename: filename,
|
|
|
|
|
CreatedAt: newTime,
|
|
|
|
|
Slug: newSlug,
|
|
|
|
|
}
|
|
|
|
|
if err := s.db.SaveUpload(ctx, &newUpload); err != nil {
|
|
|
|
|
return models.Upload{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.up.AdoptFile(site, newUpload, srcPath); err != nil {
|
|
|
|
|
return models.Upload{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return newUpload, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 11:26:40 +00:00
|
|
|
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{
|
2026-03-03 11:36:24 +00:00
|
|
|
Upload: upload,
|
|
|
|
|
CopyTemplate: s.renderCopyTemplate(upload),
|
|
|
|
|
URL: fmt.Sprintf("/sites/%v/uploads/%v/raw", site.ID, upload.ID),
|
2026-03-02 11:26:40 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2026-03-04 11:33:39 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|