@@ -0,0 +1,332 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gardomatic.kleiax.de/internal/platform/validate"
|
||||
"gardomatic.kleiax.de/internal/storage"
|
||||
)
|
||||
|
||||
type journalEntryInput struct {
|
||||
Title *string `json:"title"`
|
||||
Body *string `json:"body"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
Tags []string `json:"tags"`
|
||||
EntryType *storage.JournalEntryType `json:"entry_type"`
|
||||
}
|
||||
|
||||
func (input journalEntryInput) apply(entry *storage.JournalEntry) {
|
||||
if input.Title != nil {
|
||||
entry.Title = strings.TrimSpace(*input.Title)
|
||||
}
|
||||
if input.Body != nil {
|
||||
entry.Body = strings.TrimSpace(*input.Body)
|
||||
}
|
||||
if input.CreatedAt != nil {
|
||||
entry.CreatedAt = input.CreatedAt.UTC()
|
||||
}
|
||||
if input.EntryType != nil {
|
||||
entry.EntryType = *input.EntryType
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) createJournalEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
var input journalEntryInput
|
||||
if err := app.readJSON(w, r, &input); err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
entry := storage.JournalEntry{GardenID: gardenID, AuthorID: user.ID, AuthorName: user.Name, EntryType: storage.JournalEntryTypeJournal}
|
||||
entry.CreatedAt = time.Now().UTC()
|
||||
input.apply(&entry)
|
||||
entry.Tags = storage.NormalizeTags(input.Tags)
|
||||
if !app.validateJournalEntry(w, r, entry) {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
entry, err = app.models.Journal.Insert(entry)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
entry.Tags, err = app.saveTags(gardenID, storage.TagEntityJournal, entry.ID, entry.Tags)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
headers := make(http.Header)
|
||||
headers.Set("Location", fmt.Sprintf("/v1/gardens/%d/journal/%d", gardenID, entry.ID))
|
||||
if err = app.writeJSON(w, http.StatusCreated, envelope{"journal_entry": entry}, headers); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) listJournalEntriesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
entryType := storage.JournalEntryType(r.URL.Query().Get("type"))
|
||||
if entryType == "" {
|
||||
entryType = storage.JournalEntryTypeJournal
|
||||
}
|
||||
if entryType != storage.JournalEntryTypeJournal && entryType != storage.JournalEntryTypePinboard {
|
||||
app.badRequestResponse(w, r, fmt.Errorf("type must be journal or pinboard"))
|
||||
return
|
||||
}
|
||||
entries, err := app.models.Journal.GetAllForGarden(gardenID, entryType)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
for i := range entries {
|
||||
entries[i].Tags, err = app.loadTags(gardenID, storage.TagEntityJournal, entries[i].ID)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = app.writeJSON(w, http.StatusOK, envelope{"journal_entries": entries}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) showJournalEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
id, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
entry, err := app.models.Journal.Get(gardenID, id)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
entry.Tags, err = app.loadTags(gardenID, storage.TagEntityJournal, entry.ID)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.writeJSON(w, http.StatusOK, envelope{"journal_entry": entry}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) updateJournalEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
id, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
entry, err := app.models.Journal.Get(gardenID, id)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
var input journalEntryInput
|
||||
if err = app.readJSON(w, r, &input); err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
input.apply(&entry)
|
||||
if input.Tags != nil {
|
||||
entry.Tags = storage.NormalizeTags(input.Tags)
|
||||
} else {
|
||||
entry.Tags, err = app.loadTags(gardenID, storage.TagEntityJournal, id)
|
||||
}
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if !app.validateJournalEntry(w, r, entry) {
|
||||
return
|
||||
}
|
||||
entry, err = app.models.Journal.Update(gardenID, entry)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
if input.Tags != nil {
|
||||
entry.Tags, err = app.saveTags(gardenID, storage.TagEntityJournal, id, entry.Tags)
|
||||
}
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if stored, getErr := app.models.Journal.Get(gardenID, id); getErr == nil {
|
||||
entry.Attachments = stored.Attachments
|
||||
}
|
||||
if err = app.writeJSON(w, http.StatusOK, envelope{"journal_entry": entry}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) deleteJournalEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
id, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
if err = app.models.Journal.Delete(gardenID, id); err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (app *application) createJournalAttachmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
entryID, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
r.Body = http.MaxBytesReader(w, r.Body, storage.MaxJournalAttachmentSize+(1<<20))
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
app.badRequestResponse(w, r, fmt.Errorf("file must be provided"))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(file, storage.MaxJournalAttachmentSize+1))
|
||||
if err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if len(data) > storage.MaxJournalAttachmentSize {
|
||||
app.badRequestResponse(w, r, fmt.Errorf("file must not be larger than 25 MB"))
|
||||
return
|
||||
}
|
||||
mediaType := strings.ToLower(strings.TrimSpace(strings.Split(header.Header.Get("Content-Type"), ";")[0]))
|
||||
detected := http.DetectContentType(data)
|
||||
if mediaType == "" || mediaType == "application/octet-stream" {
|
||||
mediaType = detected
|
||||
}
|
||||
if !allowedJournalMediaType(mediaType) {
|
||||
app.badRequestResponse(w, r, fmt.Errorf("only photos, videos and audio files are supported"))
|
||||
return
|
||||
}
|
||||
name := filepath.Base(strings.ReplaceAll(header.Filename, "\\", "/"))
|
||||
if name == "." || name == "" {
|
||||
name = "attachment"
|
||||
}
|
||||
if len(name) > 255 {
|
||||
name = name[:255]
|
||||
}
|
||||
attachment, err := app.models.Journal.InsertAttachment(gardenID, entryID, storage.JournalAttachment{FileName: name, MediaType: mediaType, Data: data})
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.writeJSON(w, http.StatusCreated, envelope{"attachment": attachment}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) createJournalLibraryAttachmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
entryID, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
var input struct {
|
||||
ImageID int `json:"image_id"`
|
||||
}
|
||||
if err = app.readJSON(w, r, &input); err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if input.ImageID < 1 {
|
||||
app.badRequestResponse(w, r, fmt.Errorf("image_id must be provided"))
|
||||
return
|
||||
}
|
||||
image, err := app.models.Images.Get(gardenID, input.ImageID)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
name := image.FileName
|
||||
if name == "" {
|
||||
name = "Bild"
|
||||
}
|
||||
attachment, err := app.models.Journal.InsertAttachment(gardenID, entryID, storage.JournalAttachment{FileName: name, MediaType: image.MediaType, Size: image.Size, ImageID: &image.ID})
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.writeJSON(w, http.StatusCreated, envelope{"attachment": attachment}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func allowedJournalMediaType(value string) bool {
|
||||
switch value {
|
||||
case "image/jpeg", "image/png", "image/webp", "image/gif", "video/mp4", "video/webm", "video/quicktime", "audio/mpeg", "audio/mp4", "audio/ogg", "audio/webm", "audio/wav", "audio/x-wav":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) showJournalAttachmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
entryID, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
attachmentID, err := app.readNamedIDParam(r, "attachmentID")
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
a, err := app.models.Journal.GetAttachment(gardenID, entryID, attachmentID)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", a.MediaType)
|
||||
w.Header().Set("Content-Disposition", mime.FormatMediaType("inline", map[string]string{"filename": a.FileName}))
|
||||
w.Header().Set("Cache-Control", "private, max-age=3600")
|
||||
http.ServeContent(w, r, a.FileName, a.CreatedAt, bytes.NewReader(a.Data))
|
||||
}
|
||||
|
||||
func (app *application) deleteJournalAttachmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
entryID, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
attachmentID, err := app.readNamedIDParam(r, "attachmentID")
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
if err = app.models.Journal.DeleteAttachment(gardenID, entryID, attachmentID); err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (app *application) validateJournalEntry(w http.ResponseWriter, r *http.Request, entry storage.JournalEntry) bool {
|
||||
v := validate.New()
|
||||
storage.ValidateJournalEntry(v, entry)
|
||||
storage.ValidateTags(v, entry.Tags)
|
||||
if !v.Valid() {
|
||||
app.failedValidationResponse(w, r, v.Errors)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user