Skip to content
Snippets Groups Projects
Verified Commit fc474b14 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Further improve image APIs

parent ba0f5e5b
Branches
No related tags found
1 merge request!1Replace entire project structure
Pipeline #2577 failed
Showing
with 315 additions and 11 deletions
package api
import (
"context"
"database/sql"
"git.kuschku.de/justjanne/imghost-frontend/environment"
"github.com/gorilla/mux"
......@@ -27,6 +28,16 @@ func DeleteImage(env environment.FrontendEnvironment) http.Handler {
return
}
err = env.Storage.DeleteFiles(
context.Background(),
env.Configuration.Storage.ImageBucket,
image.Id,
)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
writer.WriteHeader(http.StatusNoContent)
})
}
......@@ -33,16 +33,21 @@ func generateId() string {
}
func determineMimeType(header string, filename string) string {
println("Determining mime header from " + header + " and " + filename)
mediaType, _, err := mime.ParseMediaType(header)
if err == nil {
println("mediatype is " + mediaType)
return mediaType
}
mediaType = mime.TypeByExtension(filepath.Ext(filename))
println("extensiontype is " + mediaType)
return mediaType
}
func determineExtension(filename string, mimeType string) (extension string, err error) {
println("determining extension for " + filename + " and " + mimeType)
extension = filepath.Ext(filename)
println("found file extension " + extension)
if extension != "" {
return
}
......@@ -56,6 +61,7 @@ func determineExtension(filename string, mimeType string) (extension string, err
return
}
extension = extensions[0]
println("found mime extension " + extension)
return
}
......@@ -81,7 +87,7 @@ func UploadImage(env environment.FrontendEnvironment) http.Handler {
}
var files []model.Image
for _, file := range request.MultipartForm.File["images[]"] {
for _, file := range request.MultipartForm.File["images"] {
println("processing file")
var image model.Image
image.Id = generateId()
......
......@@ -24,6 +24,9 @@ func NewBackendEnvironment(config configuration.BackendConfiguration) (env Backe
if env.Repositories.Images, err = repo.NewImageRepo(env.Database); err != nil {
return
}
if env.Repositories.ImageMetadata, err = repo.NewImageMetadataRepo(env.Database); err != nil {
return
}
if env.Repositories.Albums, err = repo.NewAlbumRepo(env.Database); err != nil {
return
}
......
......@@ -26,6 +26,9 @@ func NewFrontendEnvironment(config configuration.FrontendConfiguration) (env Fro
if env.Repositories.Images, err = repo.NewImageRepo(env.Database); err != nil {
return
}
if env.Repositories.ImageMetadata, err = repo.NewImageMetadataRepo(env.Database); err != nil {
return
}
if env.Repositories.Albums, err = repo.NewAlbumRepo(env.Database); err != nil {
return
}
......
......@@ -4,6 +4,7 @@ import "git.kuschku.de/justjanne/imghost-frontend/repo"
type Repositories struct {
Images repo.Images
ImageMetadata repo.ImageMetadata
Albums repo.Albums
AlbumImages repo.AlbumImages
}
......@@ -15,7 +15,7 @@ type Image struct {
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
OriginalName string `json:"original_name" db:"original_name"`
MimeType string `json:"mime_type" db:"mime_type"`
MimeType string `json:"mime_type" db:"type"`
State string `json:"state" db:"state"`
Url string `json:"url"`
}
......
package repo
import (
"git.kuschku.de/justjanne/imghost-frontend/model"
"github.com/jmoiron/sqlx"
)
type ImageMetadata struct {
db *sqlx.DB
queryList *sqlx.NamedStmt
stmtCreate *sqlx.NamedStmt
stmtDelete *sqlx.NamedStmt
}
func NewImageMetadataRepo(db *sqlx.DB) (repo ImageMetadata, err error) {
repo.db = db
repo.queryList, err = db.PrepareNamed(`
SELECT name, content
FROM image_metadata
WHERE image = :imageId
`)
if err != nil {
return
}
repo.stmtCreate, err = db.PrepareNamed(`
INSERT INTO image_metadata (image, name, content)
VALUES (:imageId, :name, :content)
`)
if err != nil {
return
}
repo.stmtDelete, err = db.PrepareNamed(`
DELETE FROM image_metadata
WHERE image = :imageId
`)
if err != nil {
return
}
return repo, nil
}
func (repo ImageMetadata) List(image model.Image) (map[string]string, error) {
rows, err := repo.queryList.Queryx(map[string]interface{}{
"imageId": image.Id,
})
if err != nil {
return nil, err
}
metadata := make(map[string]string)
for rows.Next() {
var key string
var value string
err = rows.Scan(&key, &value)
if err != nil {
return nil, err
}
metadata[key] = value
}
return metadata, nil
}
func (repo ImageMetadata) Update(imageId string, metadata map[string]string) (err error) {
tx, err := repo.db.Beginx()
if err != nil {
return
}
println("Deleting metadata for " + imageId)
_, err = tx.NamedStmt(repo.stmtDelete).Exec(map[string]interface{}{
"imageId": imageId,
})
if err != nil {
return
}
for key, value := range metadata {
println("Adding metadata for " + imageId + " with " + key + "=" + value)
_, err = tx.NamedStmt(repo.stmtCreate).Exec(map[string]interface{}{
"imageId": imageId,
"name": key,
"content": value,
})
if err != nil {
return
}
}
err = tx.Commit()
return
}
func (repo ImageMetadata) Delete(changed model.Image) (err error) {
_, err = repo.stmtDelete.Exec(map[string]interface{}{
"imageId": changed.Id,
})
return
}
......@@ -31,6 +31,7 @@ func NewImageRepo(db *sqlx.DB) (repo Images, err error) {
title,
description,
original_name,
type,
created_at,
updated_at,
state
......@@ -47,6 +48,7 @@ func NewImageRepo(db *sqlx.DB) (repo Images, err error) {
title,
description,
original_name,
type,
created_at,
updated_at,
state
......
......@@ -60,6 +60,26 @@ func (storage Storage) DownloadFile(ctx context.Context, bucketName string, file
return
}
func (storage Storage) DeleteFiles(ctx context.Context, bucketName string, prefix string) error {
objects := storage.s3client.ListObjects(
ctx,
bucketName,
minio.ListObjectsOptions{Prefix: prefix},
)
errors := storage.s3client.RemoveObjects(
ctx,
bucketName,
objects,
minio.RemoveObjectsOptions{},
)
for err := range errors {
if err.Err != nil {
return err.Err
}
}
return nil
}
func (storage Storage) UrlFor(bucketName string, fileName string) *url.URL {
fileUrl := *storage.s3client.EndpointURL()
fileUrl.Path = filepath.Join(fileUrl.Path, bucketName, fileName)
......
......@@ -11,6 +11,7 @@ import (
"github.com/justjanne/imgconv"
"gopkg.in/gographics/imagick.v2/imagick"
"io/ioutil"
"strings"
)
type ImageProcessor struct {
......@@ -26,6 +27,8 @@ func NewImageProcessor(env environment.BackendEnvironment) *ImageProcessor {
func (processor *ImageProcessor) ProcessTask(ctx context.Context, task *asynq.Task) (err error) {
var payload ImageResizePayload
if err = json.Unmarshal(task.Payload(), &payload); err != nil {
println("Could not unmarshal task")
println(err.Error())
return
}
......@@ -69,7 +72,22 @@ func (processor *ImageProcessor) ProcessTask(ctx context.Context, task *asynq.Ta
println("failed to load file: " + sourceFile.Name())
println(err.Error())
_ = processor.env.Repositories.Images.UpdateState(payload.ImageId, repo.StateError)
return err
return
}
metadata := make(map[string]string)
for _, key := range wand.GetImageProperties("exif:*") {
if strings.HasPrefix(key, "exif:thumbnail:") {
continue
}
metadata[strings.TrimPrefix(key, "exif:")] = wand.GetImageProperty(key)
}
err = processor.env.Repositories.ImageMetadata.Update(payload.ImageId, metadata)
if err != nil {
println("failed to write metadata: " + payload.ImageId)
println(err.Error())
_ = processor.env.Repositories.Images.UpdateState(payload.ImageId, repo.StateError)
return
}
err = util.LaunchGoroutines(len(payload.Sizes), func(index int) error {
......@@ -106,6 +124,18 @@ func (processor *ImageProcessor) ProcessTask(ctx context.Context, task *asynq.Ta
return
}
err = processor.env.Storage.DeleteFiles(
ctx,
processor.env.Configuration.Storage.ConversionBucket,
payload.ImageId,
)
if err != nil {
println("failed to delete temp file: " + payload.ImageId)
println(err.Error())
_ = processor.env.Repositories.Images.UpdateState(payload.ImageId, repo.StateError)
return
}
if err = processor.env.Repositories.Images.UpdateState(payload.ImageId, repo.StateDone); err != nil {
return
}
......
......@@ -3,6 +3,7 @@ import './App.css';
import ImageList from "./components/ImageList";
import {BaseUrlProvider} from './api/baseUrlContext';
import {QueryClient, QueryClientProvider} from "react-query";
import UploadView from "./components/UploadView";
const queryClient = new QueryClient();
......@@ -10,6 +11,7 @@ function App() {
return (
<BaseUrlProvider value="http://localhost:8080/">
<QueryClientProvider client={queryClient}>
<UploadView />
<ImageList/>
</QueryClientProvider>
</BaseUrlProvider>
......
import {Image} from "./model/Image";
import {useBaseUrl} from "./baseUrlContext";
import {useMutation, useQueryClient} from "react-query";
import axios from "axios";
export const useDeleteImage = () => {
const baseUrl = useBaseUrl();
const queryClient = useQueryClient();
return useMutation<unknown, unknown, Image>((image: Image) => axios.delete(
`api/v1/images/${image.id}`,
{
baseURL: baseUrl
}
), {
onSuccess: () => {
queryClient.invalidateQueries('image')
},
})
}
......@@ -6,7 +6,7 @@ import {Album} from "./model/Album";
export const useListAlbums = () => {
const baseUrl = useBaseUrl();
return useQuery(
"albums",
"album",
() => axios.get<Album[]>(
"api/v1/albums",
{
......
......@@ -6,7 +6,7 @@ import {Image} from "./model/Image";
export const useListImages = () => {
const baseUrl = useBaseUrl();
return useQuery(
"images",
"image",
() => axios.get<Image[]>(
"api/v1/images",
{
......
import {Image} from "./model/Image";
import {useBaseUrl} from "./baseUrlContext";
import {useMutation, useQueryClient} from "react-query";
import axios from "axios";
export const useUpdateImage = () => {
const baseUrl = useBaseUrl();
const queryClient = useQueryClient();
return useMutation<void, unknown, Image>((image: Image) => axios.post(
`api/v1/images/${image.id}`,
image,
{
baseURL: baseUrl
}
), {
onSuccess: () => {
queryClient.invalidateQueries('image')
},
})
}
import {Image} from "./model/Image";
import {useBaseUrl} from "./baseUrlContext";
import {useMutation, useQueryClient} from "react-query";
import axios from "axios";
export const useUploadImage = () => {
const baseUrl = useBaseUrl();
const queryClient = useQueryClient();
return useMutation<Image, unknown, FileList>((files: FileList) => {
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append("images", files[i]);
}
return axios.post<Image>(
`api/v1/images`,
formData,
{
baseURL: baseUrl
}
).then(it => it.data);
}, {
onSuccess: () => {
queryClient.invalidateQueries('image')
},
})
}
import {Image} from "../api/model/Image";
import React from "react";
import React, {useState} from "react";
import {useUpdateImage} from "../api/useUpdateImage";
import {useDeleteImage} from "../api/useDeleteImage";
export interface ImageProps {
image: Image
}
export default function ImageView({image}: ImageProps) {
const {mutate: update, error: updateError, isLoading: updateLoading} = useUpdateImage();
const {mutate: remove, error: removeError, isLoading: removeLoading} = useDeleteImage();
const [title, setTitle] = useState<string>(image.title);
const [description, setDescription] = useState<string>(image.description);
return (
<div>
<p>UpdateError: {JSON.stringify(updateError, null, 2)}</p>
<p>RemoveError: {JSON.stringify(removeError, null, 2)}</p>
<p>UpdateLoading: {JSON.stringify(updateLoading, null, 2)}</p>
<p>RemoveLoading: {JSON.stringify(removeLoading, null, 2)}</p>
<p>{image.id}</p>
<p>{image.owner}</p>
<p>{image.title}</p>
<p>{image.description}</p>
<label>
Title
<input
type="text"
value={title}
onChange={({target: {value}}) =>
setTitle(value)}
/>
</label>
<br/>
<label>
Description
<input
type="text"
value={description}
onChange={({target: {value}}) =>
setDescription(value)}
/>
</label>
<p>{image.original_name}</p>
<p>{image.mime_type}</p>
<p>{image.created_at}</p>
<p>{image.updated_at}</p>
<p>{image.state}</p>
<img src={image.url + "t"} alt=""/>
<br/>
<input
type="submit"
value="Save"
onClick={() => update({
...image,
title,
description,
})}
/>
<input
type="submit"
value="Delete"
onClick={() => remove(image)}
/>
</div>
)
}
import React from "react";
import {useUploadImage} from "../api/useUploadImage";
export default function UploadView() {
const {mutate: upload, error: uploadError, isLoading: uploadLoading} = useUploadImage();
return (
<div>
<pre>Error: {JSON.stringify(uploadError, null, 2)}</pre>
<pre>Loading: {JSON.stringify(uploadLoading, null, 2)}</pre>
<input
type="file"
onChange={async ({target}) => {
if (target.files) {
await upload(target.files)
target.files = null;
}
}}
/>
</div>
)
}
......@@ -6,6 +6,7 @@ func CorsWrapper(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Allow-Methods", "*")
if r.Method == http.MethodOptions {
w.WriteHeader(200)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment