diff --git a/.drone-manifest-local.yml b/.drone-manifest-local.yml new file mode 100644 index 0000000..d9da4dd --- /dev/null +++ b/.drone-manifest-local.yml @@ -0,0 +1,22 @@ +image: registry.nemunai.re/youp0m:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - image: registry.nemunai.re/youp0m:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 + platform: + architecture: amd64 + os: linux + - image: registry.nemunai.re/youp0m:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 + platform: + architecture: arm64 + os: linux + variant: v8 + - image: registry.nemunai.re/youp0m:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm + platform: + architecture: arm + os: linux + variant: v7 diff --git a/.drone-manifest.yml b/.drone-manifest.yml new file mode 100644 index 0000000..ade2314 --- /dev/null +++ b/.drone-manifest.yml @@ -0,0 +1,22 @@ +image: nemunaire/youp0m:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - image: nemunaire/youp0m:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 + platform: + architecture: amd64 + os: linux + - image: nemunaire/youp0m:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 + platform: + architecture: arm64 + os: linux + variant: v8 + - image: nemunaire/youp0m:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm + platform: + architecture: arm + os: linux + variant: v7 diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..358f8fc --- /dev/null +++ b/.drone.yml @@ -0,0 +1,130 @@ +--- +kind: pipeline +type: docker +name: build-arm64 + +platform: + os: linux + arch: arm64 + +steps: + - name: build + image: golang:alpine + commands: + - apk --no-cache add git go-bindata + - go generate -v + - go get -v -d + - go build -v -ldflags="-s -w" -o youp0m + - wget -O Dockerfile https://ankh.serekh.nemunai.re/local/Dockerfile-youp0m + - wget -O entrypoint.sh https://ankh.serekh.nemunai.re/local/entrypoint.sh-youp0m && chmod +x entrypoint.sh + + - name: publish + image: plugins/docker + settings: + repo: nemunaire/youp0m + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + experimental: true + squash: true + username: + from_secret: docker_username + password: + from_secret: docker_password + + - name: publish on nemunai.re + image: plugins/docker + settings: + registry: registry.nemunai.re + repo: registry.nemunai.re/youp0m + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + experimental: true + squash: true + username: + from_secret: docker_nemunai.re_username + password: + from_secret: docker_nemunai.re_password + +--- +kind: pipeline +type: docker +name: build-amd64 + +platform: + os: linux + arch: amd64 + +steps: + - name: build + image: golang:alpine + commands: + - apk --no-cache add git go-bindata + - go generate -v + - go get -v -d + - go build -v -ldflags="-s -w" -o youp0m + - wget -O Dockerfile https://ankh.serekh.nemunai.re/local/Dockerfile-youp0m + - wget -O entrypoint.sh https://ankh.serekh.nemunai.re/local/entrypoint.sh-youp0m && chmod +x entrypoint.sh + + - name: publish + image: plugins/docker + settings: + repo: nemunaire/youp0m + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + experimental: true + squash: true + username: + from_secret: docker_username + password: + from_secret: docker_password + + - name: publish on nemunai.re + image: plugins/docker + settings: + registry: registry.nemunai.re + repo: registry.nemunai.re/youp0m + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + experimental: true + squash: true + username: + from_secret: docker_nemunai.re_username + password: + from_secret: docker_nemunai.re_password + +--- +kind: pipeline +name: docker-manifest + +steps: +- name: publish + image: plugins/manifest + settings: + auto_tag: true + ignore_missing: true + spec: .drone-manifest.yml + username: + from_secret: docker_username + password: + from_secret: docker_password + +- name: publish on nemunai.re + image: plugins/manifest + settings: + auto_tag: true + ignore_missing: true + spec: .drone-manifest-local.yml + username: + from_secret: docker_nemunai.re_username + password: + from_secret: docker_nemunai.re_password + +trigger: + event: + - cron + - push + - tag + +depends_on: +- build-arm64 +- build-amd64 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..656e38e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +youp0m +vendor \ No newline at end of file diff --git a/api.go b/api.go index 66cecd0..6fcd695 100644 --- a/api.go +++ b/api.go @@ -12,21 +12,31 @@ import ( type DispatchFunction func(*User, []string, io.ReadCloser) (interface{}, error) -var apiRoutes = map[string]*(map[string]struct{AuthFunction;DispatchFunction}){ - "images": &ApiImagesRouting, - "next": &ApiNextImagesRouting, - "version": &ApiVersionRouting, -} - type apiHandler struct { - Authenticate func(*http.Request) (*User) + PE *PictureExplorer + Authenticate func(*http.Request) *User + routes map[string](map[string]struct { + AuthFunction + DispatchFunction + }) } -func ApiHandler(Authenticate func(*http.Request) (*User)) (apiHandler) { - return apiHandler{Authenticate} +func NewAPIHandler(pe *PictureExplorer, authenticate func(*http.Request) *User) *apiHandler { + return &apiHandler{ + pe, + authenticate, + map[string](map[string]struct { + AuthFunction + DispatchFunction + }){ + "images": ApiImagesRouting(pe), + "next": ApiNextImagesRouting(pe), + "version": ApiVersionRouting, + }, + } } -func (a apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (a *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { user := a.Authenticate(r) log.Printf("Handling %s API request from %s: %s %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, user, r.UserAgent()) @@ -52,8 +62,8 @@ func (a apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Route request if len(sURL) == 0 { err = errors.New(fmt.Sprintf("No action provided")) - } else if h, ok := apiRoutes[sURL[0]]; ok { - if f, ok := (*h)[r.Method]; ok { + } else if h, ok := a.routes[sURL[0]]; ok { + if f, ok := h[r.Method]; ok { if f.AuthFunction(user, sURL[1:]) { ret, err = f.DispatchFunction(user, sURL[1:], r.Body) } else { diff --git a/api_images.go b/api_images.go index 422b767..685215b 100644 --- a/api_images.go +++ b/api_images.go @@ -5,72 +5,88 @@ import ( "io" ) -var ApiImagesRouting = map[string]struct{AuthFunction; DispatchFunction}{ - "GET": {PublicPage, listImages}, - "POST": {PublicPage, addImage}, - "DELETE": {PrivatePage, hideImage}, -} - -var ApiNextImagesRouting = map[string]struct{AuthFunction; DispatchFunction}{ - "GET": {PrivatePage, listNextImages}, - "POST": {PublicPage, addImage}, - "DELETE": {PrivatePage, deleteImage}, -} - -func listImages(u *User, args []string, body io.ReadCloser) (interface{}, error) { - if len(args) < 1 { - return GetPublishedImages() - } else if args[0] == "last" { - return GetLastImage() - } else { - return GetPublishedImage(args[0]) +func ApiImagesRouting(pe *PictureExplorer) map[string]struct { + AuthFunction + DispatchFunction +} { + return map[string]struct { + AuthFunction + DispatchFunction + }{ + "GET": {PublicPage, pe.listImages}, + "POST": {PublicPage, pe.addImage}, + "DELETE": {PrivatePage, pe.hideImage}, } } -func listNextImages(u *User, args []string, body io.ReadCloser) (interface{}, error) { +func ApiNextImagesRouting(pe *PictureExplorer) map[string]struct { + AuthFunction + DispatchFunction +} { + return map[string]struct { + AuthFunction + DispatchFunction + }{ + "GET": {PrivatePage, pe.listNextImages}, + "POST": {PublicPage, pe.addImage}, + "DELETE": {PrivatePage, pe.deleteImage}, + } +} + +func (pe *PictureExplorer) listImages(u *User, args []string, body io.ReadCloser) (interface{}, error) { if len(args) < 1 { - return GetNextImages() + return pe.GetPublishedImages() + } else if args[0] == "last" { + return pe.GetLastImage() + } else { + return pe.GetPublishedImage(args[0]) + } +} + +func (pe *PictureExplorer) listNextImages(u *User, args []string, body io.ReadCloser) (interface{}, error) { + if len(args) < 1 { + return pe.GetNextImages() } else if len(args) >= 2 && args[1] == "publish" { - if pict, err := GetNextImage(args[0]); err != nil { + if pict, err := pe.GetNextImage(args[0]); err != nil { return nil, err - } else if err := pict.Publish(); err != nil { + } else if err := pe.Publish(pict); err != nil { return nil, err } else { return true, nil } } else { - return GetNextImage(args[0]) + return pe.GetNextImage(args[0]) } } -func addImage(u *User, args []string, body io.ReadCloser) (interface{}, error) { +func (pe *PictureExplorer) addImage(u *User, args []string, body io.ReadCloser) (interface{}, error) { if len(args) < 1 { return nil, errors.New("Need an image identifier to create") - } else if err := AddImage(args[0], body); err != nil { + } else if err := pe.AddImage(args[0], body); err != nil { return nil, err } else { return true, nil } } -func hideImage(u *User, args []string, body io.ReadCloser) (interface{}, error) { +func (pe *PictureExplorer) hideImage(u *User, args []string, body io.ReadCloser) (interface{}, error) { if len(args) < 1 { return nil, errors.New("Need an image identifier to delete") - } else if pict, err := GetPublishedImage(args[0]); err != nil { + } else if pict, err := pe.GetPublishedImage(args[0]); err != nil { return nil, errors.New("No matching image") - } else if err := pict.Unpublish(); err != nil { + } else if err := pe.Unpublish(pict); err != nil { return nil, err } else { return true, nil } } -func deleteImage(u *User, args []string, body io.ReadCloser) (interface{}, error) { +func (pe *PictureExplorer) deleteImage(u *User, args []string, body io.ReadCloser) (interface{}, error) { if len(args) < 1 { return nil, errors.New("Need an image identifier to delete") - } else if pict, err := GetNextImage(args[0]); err != nil { + } else if pict, err := pe.GetNextImage(args[0]); err != nil { return nil, errors.New("No matching image") - } else if err := pict.Remove(); err != nil { + } else if err := pe.Remove(pict); err != nil { return nil, err } else { return true, nil diff --git a/assets-dev.go b/assets-dev.go new file mode 100644 index 0000000..549607f --- /dev/null +++ b/assets-dev.go @@ -0,0 +1,32 @@ +//go:build dev +// +build dev + +package main + +import ( + "flag" + "net/http" + "os" + "path/filepath" +) + +var ( + Assets http.FileSystem + StaticDir string = "static/" +) + +func init() { + flag.StringVar(&StaticDir, "static", StaticDir, "Directory containing static files") +} + +func sanitizeStaticOptions() error { + StaticDir, _ = filepath.Abs(StaticDir) + if _, err := os.Stat(StaticDir); os.IsNotExist(err) { + StaticDir, _ = filepath.Abs(filepath.Join(filepath.Dir(os.Args[0]), "static")) + if _, err := os.Stat(StaticDir); os.IsNotExist(err) { + return err + } + } + Assets = http.Dir(StaticDir) + return nil +} diff --git a/assets.go b/assets.go new file mode 100644 index 0000000..1841ebe --- /dev/null +++ b/assets.go @@ -0,0 +1,28 @@ +//go:build !dev +// +build !dev + +package main + +import ( + "embed" + "io/fs" + "log" + "net/http" +) + +//go:embed static/* static/js/* static/css/* +var _assets embed.FS + +var Assets http.FileSystem + +func init() { + sub, err := fs.Sub(_assets, "static") + if err != nil { + log.Fatal("Unable to cd to static/ directory:", err) + } + Assets = http.FS(sub) +} + +func sanitizeStaticOptions() error { + return nil +} diff --git a/auth.go b/auth.go index 96fe98d..0860bd9 100644 --- a/auth.go +++ b/auth.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/nyarla/go-crypt" + "gitlab.com/nyarla/go-crypt" ) type User struct { @@ -52,10 +52,9 @@ func (h Htpasswd) Authenticate(username, password string) *User { } } - /// Request authentication -func Authenticate(htpasswd Htpasswd, r *http.Request) (*User) { +func Authenticate(htpasswd Htpasswd, r *http.Request) *User { // Authenticate the user if any if username, password, ok := r.BasicAuth(); ok { return htpasswd.Authenticate(username, password) @@ -64,7 +63,6 @@ func Authenticate(htpasswd Htpasswd, r *http.Request) (*User) { } } - /// Page rules type AuthFunction func(*User, []string) bool diff --git a/backend.go b/backend.go new file mode 100644 index 0000000..e5d2073 --- /dev/null +++ b/backend.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "image" + "io" + "net/http" + "strings" +) + +var existing_backends []string + +type BackendSelector string + +func (s *BackendSelector) String() string { + return string(*s) +} + +func (s *BackendSelector) Set(v string) error { + found := false + for _, b := range existing_backends { + if strings.ToLower(v) == b { + found = true + tmp := BackendSelector(v) + s = &tmp + break + } + } + + if !found { + return fmt.Errorf("%q is not a known file backend (existing backends: %v)", v, existing_backends) + } + return nil +} + +type FileBackend interface { + DeletePicture(string, string) error + ServeFile() http.Handler + GetPicture(string, string, io.Writer) error + GetPictureInfo(string, string) (*Picture, error) + ListPictures(string) ([]*Picture, error) + MovePicture(string, string, string) error + PutPicture(string, string, *image.Image) error +} diff --git a/backend_local.go b/backend_local.go new file mode 100644 index 0000000..55aa353 --- /dev/null +++ b/backend_local.go @@ -0,0 +1,106 @@ +package main + +import ( + "image" + "image/jpeg" + "io" + "io/ioutil" + "net/http" + "os" + "path" + "time" +) + +func init() { + existing_backends = append(existing_backends, "local") +} + +type LocalFileBackend struct { + BaseDir string +} + +func (l *LocalFileBackend) getPath(box, name string) string { + return path.Join(l.BaseDir, box, name+".jpg") +} + +func (l *LocalFileBackend) DeletePicture(box, name string) error { + return os.Remove(l.getPath(box, name)) +} + +func (l *LocalFileBackend) ServeFile() http.Handler { + return http.FileServer(http.Dir(l.BaseDir)) +} + +func (l *LocalFileBackend) createPictureInfo(local_path, name string, file os.FileInfo) *Picture { + return &Picture{ + local_path, // Path + name, // Basename + name[:len(name)-len(path.Ext(name))], // Sanitized filename + file.ModTime(), // UploadTime + } +} + +func (l *LocalFileBackend) GetPicture(box, name string, w io.Writer) error { + local_path := l.getPath(box, name) + + fd, err := os.Open(local_path) + if err != nil { + return err + } + defer fd.Close() + + _, err = io.Copy(w, fd) + + return err +} + +func (l *LocalFileBackend) GetPictureInfo(box, name string) (*Picture, error) { + local_path := l.getPath(box, name) + + file, err := os.Stat(local_path) + if err != nil { + return nil, err + } + + return l.createPictureInfo(path.Join(box, name+".jpg"), name, file), nil +} + +func (l *LocalFileBackend) ListPictures(box string) ([]*Picture, error) { + files, err := ioutil.ReadDir(path.Join(l.BaseDir, box)) + if err != nil { + return nil, err + } + + pictures := make([]*Picture, 0) + for _, file := range files { + if !file.IsDir() { + pictures = append(pictures, l.createPictureInfo(path.Join(box, file.Name()), file.Name(), file)) + } + } + + return pictures, nil +} + +func (l *LocalFileBackend) MovePicture(box_from, box_to, name string) error { + newpath := l.getPath(box_to, name) + + err := os.Rename( + l.getPath(box_from, name), + newpath, + ) + if err != nil { + return err + } + + return os.Chtimes(newpath, time.Now(), time.Now()) +} + +func (l *LocalFileBackend) PutPicture(box, name string, img *image.Image) error { + fd, err := os.OpenFile(l.getPath(box, name), os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return err + } + defer fd.Close() + + return jpeg.Encode(fd, *img, nil) +} diff --git a/backend_s3.go b/backend_s3.go new file mode 100644 index 0000000..4d6c589 --- /dev/null +++ b/backend_s3.go @@ -0,0 +1,272 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "image" + "image/jpeg" + "io" + "log" + "net/http" + "os" + "path" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" +) + +var ( + s3_endpoint string + s3_region = "us" + s3_bucket string + s3_access_key string + s3_secret_key string + s3_path_style bool +) + +func init() { + existing_backends = append(existing_backends, "s3") + + if endpoint, ok := os.LookupEnv("S3_ENDPOINT"); ok { + backend = "s3" + s3_endpoint = endpoint + } + if region, ok := os.LookupEnv("S3_REGION"); ok { + backend = "s3" + s3_region = region + } + s3_bucket, _ = os.LookupEnv("S3_BUCKET") + s3_access_key, _ = os.LookupEnv("S3_ACCESS_KEY") + s3_secret_key, _ = os.LookupEnv("S3_SECRET_KEY") + if path_style, ok := os.LookupEnv("S3_PATH_STYLE"); ok { + s3_path_style = path_style == "1" || path_style == "ON" || path_style == "on" || path_style == "TRUE" || path_style == "true" || path_style == "yes" || path_style == "YES" + } + + flag.StringVar(&s3_endpoint, "s3-endpoint", s3_endpoint, "When using S3 backend, endpoint to use") + flag.StringVar(&s3_region, "s3-region", s3_region, "When using S3 backend, region to use") + flag.StringVar(&s3_bucket, "s3-bucket", s3_bucket, "When using S3 backend, bucket to use") + flag.StringVar(&s3_access_key, "s3-access-key", s3_access_key, "When using S3 backend, Access Key") + flag.StringVar(&s3_secret_key, "s3-secret-key", s3_secret_key, "When using S3 backend, Secret Key") + flag.BoolVar(&s3_path_style, "s3-path-style", s3_path_style, "When using S3 backend, force path style (when using minio)") +} + +type S3FileBackend struct { + Endpoint string + Region string + Bucket string + AccessKey string + SecretKey string + PathStyle bool + BaseDir string +} + +func (l *S3FileBackend) getPath(box, name string) string { + if l.BaseDir == "" { + return path.Join(box, name+".jpg") + } else { + return path.Join(l.BaseDir, box, name+".jpg") + } +} + +func (l *S3FileBackend) newSession() (*session.Session, error) { + return session.NewSession(&aws.Config{ + Credentials: credentials.NewStaticCredentials(l.AccessKey, l.SecretKey, ""), + Endpoint: &l.Endpoint, + Region: &l.Region, + S3ForcePathStyle: &l.PathStyle, + }) +} + +func (l *S3FileBackend) DeletePicture(box, name string) error { + s, err := l.newSession() + if err != nil { + return err + } + + log.Println(l.getPath(box, name)) + + input := &s3.DeleteObjectsInput{ + Bucket: aws.String(l.Bucket), + Delete: &s3.Delete{ + Objects: []*s3.ObjectIdentifier{ + { + Key: aws.String(l.getPath(box, name)), + }, + }, + }, + } + + _, err = s3.New(s).DeleteObjects(input) + return err +} + +func (l *S3FileBackend) ServeFile() http.Handler { + return l +} + +func (l *S3FileBackend) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s, err := l.newSession() + if err != nil { + return + } + + result, err := s3.New(s).GetObject(&s3.GetObjectInput{ + Bucket: aws.String(l.Bucket), + Key: aws.String(r.URL.Path), + }) + if err != nil { + return + } + + io.Copy(w, result.Body) +} + +func (l *S3FileBackend) createPictureInfo(local_path, name string, file *s3.Object) *Picture { + return &Picture{ + local_path, // Path + name, // Basename + name[:len(name)-len(path.Ext(name))], // Sanitized filename + *file.LastModified, // UploadTime + } +} + +func (l *S3FileBackend) GetPicture(box, name string, w io.Writer) error { + s, err := l.newSession() + if err != nil { + return err + } + + s3_path := l.getPath(box, name) + + input := &s3.GetObjectInput{ + Bucket: aws.String(l.Bucket), + Key: aws.String(s3_path), + } + + result, err := s3.New(s).GetObject(input) + if err != nil { + return err + } + + _, err = io.Copy(w, result.Body) + + return err +} + +func (l *S3FileBackend) GetPictureInfo(box, name string) (*Picture, error) { + s, err := l.newSession() + if err != nil { + return nil, err + } + + input := &s3.ListObjectsInput{ + Bucket: aws.String(l.Bucket), + Prefix: aws.String(l.getPath(box, name)), + } + + result, err := s3.New(s).ListObjects(input) + if err != nil { + return nil, err + } + + var pictures []*Picture + for _, file := range result.Contents { + pictures = append(pictures, l.createPictureInfo(*file.Key, path.Base(*file.Key), file)) + break + } + + if len(pictures) == 0 { + return nil, fmt.Errorf("Object not found") + } + + return pictures[0], nil +} + +func (l *S3FileBackend) ListPictures(box string) ([]*Picture, error) { + s, err := l.newSession() + if err != nil { + return nil, err + } + + input := &s3.ListObjectsInput{ + Bucket: aws.String(l.Bucket), + } + + if l.BaseDir == "" { + input.Prefix = aws.String(box) + } else { + input.Prefix = aws.String(path.Join(l.BaseDir, box)) + } + + result, err := s3.New(s).ListObjects(input) + if err != nil { + return nil, err + } + + pictures := make([]*Picture, 0) + for _, file := range result.Contents { + pictures = append(pictures, l.createPictureInfo(path.Join(box, *file.Key), path.Base(*file.Key), file)) + } + + return pictures, nil +} + +func (l *S3FileBackend) MovePicture(box_from, box_to, name string) error { + s, err := l.newSession() + if err != nil { + return err + } + + log.Println(l.getPath(box_from, name), l.getPath(box_to, name)) + + _, err = s3.New(s).CopyObject(&s3.CopyObjectInput{ + Bucket: aws.String(l.Bucket), + CopySource: aws.String(path.Join("", l.Bucket, l.getPath(box_from, name))), + Key: aws.String(l.getPath(box_to, name)), + }) + if err != nil { + return err + } + + log.Println(l.getPath(box_from, name)) + + input := &s3.DeleteObjectsInput{ + Bucket: aws.String(l.Bucket), + Delete: &s3.Delete{ + Objects: []*s3.ObjectIdentifier{ + { + Key: aws.String(l.getPath(box_from, name)), + }, + }, + }, + } + + _, err = s3.New(s).DeleteObjects(input) + return err +} + +func (l *S3FileBackend) PutPicture(box, name string, img *image.Image) error { + s, err := l.newSession() + if err != nil { + return err + } + + bbuf := new(bytes.Buffer) + err = jpeg.Encode(bbuf, *img, nil) + if err != nil { + return err + } + + _, err = s3manager.NewUploader(s).Upload(&s3manager.UploadInput{ + Bucket: aws.String(l.Bucket), + ACL: aws.String("public-read"), + Key: aws.String(l.getPath(box, name)), + Body: bbuf, + }) + + return err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..20b88a8 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.nemunai.re/youp0m + +go 1.16 + +require ( + github.com/aws/aws-sdk-go v1.44.136 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 + gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7195d1a --- /dev/null +++ b/go.sum @@ -0,0 +1,108 @@ +github.com/aws/aws-sdk-go v1.44.91 h1:SRWmuX7PTyhBdLuvSfM7KWrWISJsrRsUPcFDSFduRxY= +github.com/aws/aws-sdk-go v1.44.91/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.95 h1:QwmA+PeR6v4pF0f/dPHVPWGAshAhb9TnGZBTM5uKuI8= +github.com/aws/aws-sdk-go v1.44.95/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.96 h1:S9paaqnJ0AJ95t5AB+iK8RM6YNZN0W0Lek1gOVJsEr8= +github.com/aws/aws-sdk-go v1.44.96/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.97 h1:lxgxp7d6uuGsP7jHKIX3GHd7ExFigCIF04VuKf8XUII= +github.com/aws/aws-sdk-go v1.44.97/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.98 h1:fX+NxebSdO/9T6DTNOLhpC+Vv6RNkKRfsMg0a7o/yBo= +github.com/aws/aws-sdk-go v1.44.98/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.99 h1:ITZ9q/fmH+Ksaz2TbyMU2d19vOOWs/hAlt8NbXAieHw= +github.com/aws/aws-sdk-go v1.44.99/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.100 h1:7I86bWNQB+HGDT5z/dJy61J7qgbgLoZ7O51C9eL6hrA= +github.com/aws/aws-sdk-go v1.44.100/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.101 h1:O/em5aIxKI/FkwcWAFKEY+JhPDCRsqoVUC6xEF4tGic= +github.com/aws/aws-sdk-go v1.44.101/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.102 h1:6tUCTGL2UDbFZae1TLGk8vTgeXuzkb8KbAe2FiAeKHc= +github.com/aws/aws-sdk-go v1.44.102/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.103 h1:tbhBHKgiZSIUkG8FcHy3wYKpPVvp65Wn7ZiX0B8phpY= +github.com/aws/aws-sdk-go v1.44.103/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.104 h1:NiPYL60aOSH0TsAzQngx/aBdxC12TXhgw07DQFh76GU= +github.com/aws/aws-sdk-go v1.44.104/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.105 h1:UUwoD1PRKIj3ltrDUYTDQj5fOTK3XsnqolLpRTMmSEM= +github.com/aws/aws-sdk-go v1.44.105/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.109 h1:+Na5JPeS0kiEHoBp5Umcuuf+IDqXqD0lXnM920E31YI= +github.com/aws/aws-sdk-go v1.44.109/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.115 h1:qFYIx97cT3k54Bn/lfM6idHbqRHILJyG0SY/0qlKiG0= +github.com/aws/aws-sdk-go v1.44.115/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.116 h1:NpLIhcvLWXJZAEwvPj3TDHeqp7DleK6ZUVYyW01WNHY= +github.com/aws/aws-sdk-go v1.44.116/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.117 h1:mZuODB3Y4soG9QWAXyGb2po+6Easa/enifpj4MnZ91s= +github.com/aws/aws-sdk-go v1.44.117/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.118 h1:FJOqIRTukf7+Ulp047/k7JB6eqMXNnj7eb+coORThHQ= +github.com/aws/aws-sdk-go v1.44.118/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.119 h1:TPkpDsanBMcZaF5wHwpKhjkapRV/b7d2qdC+a+IPbmY= +github.com/aws/aws-sdk-go v1.44.119/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.120 h1:dsOxGf17H9hCVCA4aWpFWEcJMHkX+Uw7l4pGcxb27wM= +github.com/aws/aws-sdk-go v1.44.120/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.121 h1:ahBRUqUp4qLyGmSM5KKn+TVpZkRmtuLxTWw+6Hq/ebs= +github.com/aws/aws-sdk-go v1.44.121/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.123 h1:+vVGJ7+vQU6/wRcgRwSBBrIuG/lLL/0LB3HlN5jFv3c= +github.com/aws/aws-sdk-go v1.44.123/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.124 h1:Xe1WQRUUekZf6ZFm3SD0vplB/AP/hymVqMiRS9LQRIs= +github.com/aws/aws-sdk-go v1.44.124/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.125 h1:yIyCs6HX1BOj6SFTirvBwVM1tTfplKrJOyilIZPtKV8= +github.com/aws/aws-sdk-go v1.44.125/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.126 h1:7HQJw2DNiwpxqMe2H7odGNT2rhO4SRrUe5/8dYXl0Jk= +github.com/aws/aws-sdk-go v1.44.126/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.127 h1:IoO2VfuIQg1aMXnl8l6OpNUKT4Qq5CnJMOyIWoTYXj0= +github.com/aws/aws-sdk-go v1.44.127/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.128 h1:X34pX5t0LIZXjBY11yf9JKMP3c1aZgirh+5PjtaZyJ4= +github.com/aws/aws-sdk-go v1.44.128/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.129 h1:yld8Rc8OCahLtenY1mnve4w1jVeBu/rSiscGzodaDOs= +github.com/aws/aws-sdk-go v1.44.129/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.130 h1:a/qwOxmYJF47xTZvTjECSJXnfRbjegb3YxvCXfETtnY= +github.com/aws/aws-sdk-go v1.44.130/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.132 h1:+IjL9VoR0OXScQ5gyme9xjcolwUkd3uaH144f4Ao+4s= +github.com/aws/aws-sdk-go v1.44.132/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.133 h1:+pWxt9nyKc0jf33rORBaQ93KPjYpmIIy3ozVXdJ82Oo= +github.com/aws/aws-sdk-go v1.44.133/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.135 h1:DJJP/CkEpgafA5p5jlY9VzDRyKrfABVixzIxrK/3tWU= +github.com/aws/aws-sdk-go v1.44.135/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.136 h1:J1KJJssa8pjU8jETYUxwRS37KTcxjACfKd9GK8t+5ZU= +github.com/aws/aws-sdk-go v1.44.136/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/images.go b/images.go index a18baf6..af74ce7 100644 --- a/images.go +++ b/images.go @@ -6,14 +6,14 @@ import ( "strings" ) -type imagesHandler struct{ +type imagesHandler struct { name string - auth func(*http.Request) (bool) + auth func(*http.Request) bool hndlr http.Handler - getter func(fname string) (Picture, error) + getter func(fname string) (*Picture, error) } -func ImagesHandler(name string, auth func(*http.Request) (bool), hndlr http.Handler, getter func(fname string) (Picture, error)) (imagesHandler) { +func ImagesHandler(name string, auth func(*http.Request) bool, hndlr http.Handler, getter func(fname string) (*Picture, error)) imagesHandler { return imagesHandler{name, auth, hndlr, getter} } @@ -39,7 +39,7 @@ func (i imagesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Check rights - if ! i.auth(r) { + if !i.auth(r) { w.Header().Set("WWW-Authenticate", "Basic realm=\"YouP0m\"") http.Error(w, "You are not allowed to perform this request.", http.StatusUnauthorized) return @@ -50,7 +50,7 @@ func (i imagesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusNotFound) return } else { - r.URL.Path = "/" + pict.basename + r.URL.Path = "/" + pict.path i.hndlr.ServeHTTP(w, r) } } diff --git a/main.go b/main.go index 1ca07d2..7203564 100644 --- a/main.go +++ b/main.go @@ -2,27 +2,31 @@ package main import ( "flag" + "fmt" "log" "net/http" "os" - "path/filepath" + "path" ) -var PublishedImgDir string -var NextImgDir string -var ThumbsDir string +var mux = http.NewServeMux() + +var ( + ThumbsDir string + backend = "local" +) func main() { bind := flag.String("bind", ":8080", "Bind port/socket") htpasswd_file := flag.String("htpasswd", "", "Admin passwords file, Apache htpasswd format") - flag.StringVar(&PublishedImgDir, "publishedimgdir", "./images/published/", "Directory where save published pictures") - flag.StringVar(&NextImgDir, "nextimgdir", "./images/next/", "Directory where save pictures to review") + publishedImgDir := flag.String("publishedimgdir", "published/", "Directory where save published pictures") + nextImgDir := flag.String("nextimgdir", "next/", "Directory where save pictures to review") + baseDir := flag.String("basedir", "images/", "Local base directory where find published and next dirs") + flag.StringVar(&backend, "storage-backend", backend, fmt.Sprintf("Storage backend to use (between: %s)", existing_backends)) flag.StringVar(&ThumbsDir, "thumbsdir", "./images/thumbs/", "Directory where generate thumbs") flag.Parse() - htpasswd := &Htpasswd{ - entries: map[string]string{"admin": "2fClb0C8dIphk"}, - } + htpasswd := &Htpasswd{} if htpasswd_file != nil && *htpasswd_file != "" { log.Println("Reading htpasswd file...") @@ -30,21 +34,12 @@ func main() { if htpasswd, err = NewHtpasswd(*htpasswd_file); htpasswd == nil { log.Fatal("Unable to parse htpasswd:", err) } - } else { - log.Println("Using default credentials for administrative part: admin:admin") + } else if *nextImgDir == "next/" { + log.Println("Disable admin interface, images will be published without moderation") + nextImgDir = publishedImgDir } log.Println("Checking paths...") - if _, err := os.Stat(PublishedImgDir); os.IsNotExist(err) { - if err := os.MkdirAll(PublishedImgDir, 0755); err != nil { - log.Fatal(err) - } - } - if _, err := os.Stat(NextImgDir); os.IsNotExist(err) { - if err := os.MkdirAll(NextImgDir, 0755); err != nil { - log.Fatal(err) - } - } if _, err := os.Stat(ThumbsDir); os.IsNotExist(err) { if err := os.MkdirAll(ThumbsDir, 0755); err != nil { log.Fatal(err) @@ -52,47 +47,69 @@ func main() { } log.Println("Registering handlers...") - authFunc := func (r *http.Request) (*User){ return Authenticate(*htpasswd, r) } + authFunc := func(r *http.Request) *User { return Authenticate(*htpasswd, r) } - staticDir, _ := filepath.Abs("static") - if _, err := os.Stat(staticDir); os.IsNotExist(err) { - staticDir, _ = filepath.Abs(filepath.Join(filepath.Dir(os.Args[0]), "static")) - if _, err := os.Stat(staticDir); os.IsNotExist(err) { - log.Fatal(err) - } + if err := sanitizeStaticOptions(); err != nil { + log.Fatal(err) } - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, filepath.Join(staticDir, "index.html")) }) - mux.Handle("/css/", http.FileServer(http.Dir(staticDir))) - mux.Handle("/js/", http.FileServer(http.Dir(staticDir))) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + r.URL.Path = "/" + http.FileServer(Assets).ServeHTTP(w, r) + }) - mux.Handle("/api/", http.StripPrefix("/api", ApiHandler(authFunc))) + var storage_backend FileBackend + if backend == "local" { + storage_backend = &LocalFileBackend{*baseDir} + if _, err := os.Stat(path.Join(*baseDir, *publishedImgDir)); os.IsNotExist(err) { + if err := os.MkdirAll(path.Join(*baseDir, *publishedImgDir), 0755); err != nil { + log.Fatal(err) + } + } + if _, err := os.Stat(path.Join(*baseDir, *nextImgDir)); os.IsNotExist(err) { + if err := os.MkdirAll(path.Join(*baseDir, *nextImgDir), 0755); err != nil { + log.Fatal(err) + } + } + } else if backend == "s3" { + storage_backend = &S3FileBackend{s3_endpoint, s3_region, s3_bucket, s3_access_key, s3_secret_key, s3_path_style, *baseDir} + } else { + log.Fatalf("%q is not a valid storage backend.", backend) + } + log.Printf("Using %s storage backend", backend) + + pe := &PictureExplorer{ + FileBackend: storage_backend, + PublishedImgDir: *publishedImgDir, + NextImgDir: *nextImgDir, + } + + mux.Handle("/api/", http.StripPrefix("/api", NewAPIHandler(pe, authFunc))) mux.Handle("/images/", http.StripPrefix("/images", ImagesHandler( "images", - func (*http.Request) (bool){ return true; }, - http.FileServer(http.Dir(PublishedImgDir)), - GetPublishedImage, + func(*http.Request) bool { return true }, + pe.ServeFile(), + pe.GetPublishedImage, ))) mux.Handle("/images/next/", http.StripPrefix("/images/next", ImagesHandler( "next", - func (r *http.Request) (bool){ return authFunc(r) != nil; }, - http.FileServer(http.Dir(NextImgDir)), - GetNextImage, + func(r *http.Request) bool { return authFunc(r) != nil }, + pe.ServeFile(), + pe.GetNextImage, ))) mux.Handle("/images/thumbs/", http.StripPrefix("/images/thumbs", ImagesHandler( "thumbs", - func (*http.Request) (bool){ return true; }, + func(*http.Request) bool { return true }, http.FileServer(http.Dir(ThumbsDir)), - GetPublishedImage, + pe.GetPublishedImage, ))) mux.Handle("/images/next/thumbs/", http.StripPrefix("/images/next/thumbs", ImagesHandler( "nexthumbs", - func (r *http.Request) (bool){ return authFunc(r) != nil; }, + func(r *http.Request) bool { return authFunc(r) != nil }, http.FileServer(http.Dir(ThumbsDir)), - GetNextImage, + pe.GetNextImage, ))) mux.HandleFunc("/admin/", func(w http.ResponseWriter, r *http.Request) { @@ -100,7 +117,8 @@ func main() { w.Header().Set("WWW-Authenticate", "Basic realm=\"YouP0m\"") http.Error(w, "You are not allowed to perform this request.", http.StatusUnauthorized) } else { - http.ServeFile(w, r, filepath.Join(staticDir, "admin.html")) + r.URL.Path = "/admin.html" + http.FileServer(Assets).ServeHTTP(w, r) } }) diff --git a/picture.go b/picture.go index ac05793..42fb39a 100644 --- a/picture.go +++ b/picture.go @@ -1,22 +1,27 @@ package main import ( + "encoding/base64" "errors" - "io" - "io/ioutil" "image" _ "image/gif" "image/jpeg" _ "image/png" + "io" "os" - "path/filepath" + "path" "sort" - "strconv" "time" "github.com/nfnt/resize" ) +type PictureExplorer struct { + FileBackend + PublishedImgDir string + NextImgDir string +} + type Picture struct { path string basename string @@ -24,143 +29,92 @@ type Picture struct { UploadTime time.Time `json:"upload_time"` } -type ByUploadTime []Picture +type ByUploadTime []*Picture -func (a ByUploadTime) Len() int { return len(a) } +func (a ByUploadTime) Len() int { return len(a) } func (a ByUploadTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByUploadTime) Less(i, j int) bool { return a[i].UploadTime.Sub(a[j].UploadTime).Nanoseconds() < 0 } +func (a ByUploadTime) Less(i, j int) bool { + return a[i].UploadTime.Sub(a[j].UploadTime).Nanoseconds() < 0 +} -func getImages(dir string) ([]Picture, error) { - if files, err := ioutil.ReadDir(dir); err != nil { +func (e *PictureExplorer) GetNextImages() (pictures []*Picture, err error) { + pictures, err = e.ListPictures(e.NextImgDir) + sort.Sort(ByUploadTime(pictures)) + return +} + +func (e *PictureExplorer) GetPublishedImages() (pictures []*Picture, err error) { + pictures, err = e.ListPictures(e.PublishedImgDir) + sort.Sort(ByUploadTime(pictures)) + return +} + +func (e *PictureExplorer) GetLastImage() (*Picture, error) { + picts, err := e.GetPublishedImages() + if err != nil { return nil, err - } else { - pictures := make([]Picture, 0) - for _, file := range files { - if !file.IsDir() { - filename := file.Name() - pictures = append(pictures, Picture{ - filepath.Join(dir, filename), // Path - filename, // Basename - filename[:len(filename) - len(filepath.Ext(filename))], // Sanitized filename - file.ModTime(), // UploadTime - }) - } - } - sort.Sort(ByUploadTime(pictures)) - return pictures, nil } + + return picts[len(picts)-1], nil } -func GetNextImages() ([]Picture, error) { - return getImages(NextImgDir) +func (e *PictureExplorer) GetPublishedImage(fname string) (*Picture, error) { + return e.GetPictureInfo(e.PublishedImgDir, fname) } -func GetPublishedImages() ([]Picture, error) { - return getImages(PublishedImgDir) +func (e *PictureExplorer) GetNextImage(fname string) (*Picture, error) { + return e.GetPictureInfo(e.NextImgDir, fname) } -func GetLastImage() (Picture, error) { - if picts, err := GetPublishedImages(); err != nil { - return Picture{}, err - } else { - return picts[len(picts)-1], nil - } -} - -func getImage(flist func() ([]Picture, error), fname string) (Picture, error) { - if picts, err := flist(); err != nil { - return Picture{}, err - } else if pid, err := strconv.Atoi(fname); err == nil { - if pid < len(picts) { - return picts[pid], nil - } else { - return Picture{}, errors.New("Invalid picture identifier") - } - } else { - for _, pict := range picts { - if pict.Name == fname || pict.basename == fname { - return pict, nil - } - } - return Picture{}, errors.New("No such picture") - } -} - -func GetPublishedImage(fname string) (Picture, error) { - return getImage(GetPublishedImages, fname) -} - -func GetNextImage(fname string) (Picture, error) { - return getImage(GetNextImages, fname) -} - -func UniqueImage(filename string) (bool) { - if pict, _ := GetPublishedImage(filename); pict.path != "" { +func (e *PictureExplorer) IsUniqueName(filename string) bool { + if pict, _ := e.GetPublishedImage(filename); pict != nil { return false - } else { - if pict, _ := GetNextImage(filename); pict.path != "" { - return false - } else { - return true - } } + + if pict, _ := e.GetNextImage(filename); pict != nil { + return false + } + + return true } -func AddImage(filename string, blob io.ReadCloser) (error) { +func (e *PictureExplorer) AddImage(filename string, blob io.ReadCloser) error { // Check the name is not already used - if ok := UniqueImage(filename); !ok { + if ok := e.IsUniqueName(filename); !ok { return errors.New("This filename is already used, please choose another one.") + } // Convert to JPEG - } else if img, _, err := image.Decode(blob); err != nil { + img, _, err := image.Decode(base64.NewDecoder(base64.StdEncoding, blob)) + if err != nil { return err + } // Save file - } else if fw, err := os.Create(filepath.Join(NextImgDir, filename + ".jpg")); err != nil { + if err := e.PutPicture(e.NextImgDir, filename, &img); err != nil { return err - } else if err := jpeg.Encode(fw, img, nil); err != nil { - return err - } else { - fw.Close() - - thumb := resize.Thumbnail(300, 185, img, resize.Lanczos3) - - // Save thumbnail - if fw, err := os.Create(filepath.Join(ThumbsDir, filename + ".jpg")); err != nil { - return err - } else if err := jpeg.Encode(fw, thumb, nil); err != nil { - return err - } else { - fw.Close() - } } - return nil + thumb := resize.Thumbnail(300, 185, img, resize.Lanczos3) + + // Save thumbnail + fw, err := os.Create(path.Join(ThumbsDir, filename+".jpg")) + if err != nil { + return err + } + defer fw.Close() + + return jpeg.Encode(fw, thumb, nil) } -func (p Picture) Publish() (error) { - npath := filepath.Join(PublishedImgDir, p.basename) - if err := os.Rename(p.path, npath); err != nil { - return err - } else if err := os.Chtimes(npath, time.Now(), time.Now()); err != nil { - return err - } else { - return nil - } +func (e *PictureExplorer) Publish(p *Picture) error { + return e.MovePicture(e.NextImgDir, e.PublishedImgDir, p.Name) } -func (p Picture) Unpublish() (error) { - if err := os.Rename(p.path, filepath.Join(NextImgDir, p.basename)); err != nil { - return err - } else { - return nil - } +func (e *PictureExplorer) Unpublish(p *Picture) error { + return e.MovePicture(e.PublishedImgDir, e.NextImgDir, p.Name) } -func (p Picture) Remove() (error) { - if err := os.Remove(p.path); err != nil { - return err - } else { - return nil - } +func (e *PictureExplorer) Remove(p *Picture) error { + return e.DeletePicture(path.Dir(p.path), p.Name) } diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..7979833 --- /dev/null +++ b/renovate.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "packageRules": [ + { + "matchPackageNames": ["github.com/aws/aws-sdk-go"], + "automerge": true, + "automergeType": "branch" + } + ] +} diff --git a/static.go b/static.go new file mode 100644 index 0000000..f8525be --- /dev/null +++ b/static.go @@ -0,0 +1,14 @@ +package main + +import ( + "net/http" +) + +func init() { + mux.HandleFunc("/css/", func(w http.ResponseWriter, r *http.Request) { + http.FileServer(Assets).ServeHTTP(w, r) + }) + mux.HandleFunc("/js/", func(w http.ResponseWriter, r *http.Request) { + http.FileServer(Assets).ServeHTTP(w, r) + }) +} diff --git a/static/admin.html b/static/admin.html index c1b1445..2fc8f91 100644 --- a/static/admin.html +++ b/static/admin.html @@ -4,9 +4,6 @@