diff --git a/api.go b/api.go index d955498..6fcd695 100644 --- a/api.go +++ b/api.go @@ -12,24 +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 { + 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()) @@ -55,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 a54d8f2..685215b 100644 --- a/api_images.go +++ b/api_images.go @@ -5,78 +5,88 @@ import ( "io" ) -var ApiImagesRouting = map[string]struct { +func ApiImagesRouting(pe *PictureExplorer) 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]) +} { + 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/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..ade2f86 --- /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 + } + + var pictures []*Picture + 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/images.go b/images.go index 56b38eb..af74ce7 100644 --- a/images.go +++ b/images.go @@ -10,10 +10,10 @@ type imagesHandler struct { name string 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} } @@ -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 994bd66..dbbe066 100644 --- a/main.go +++ b/main.go @@ -9,15 +9,13 @@ import ( var mux = http.NewServeMux() -var PublishedImgDir string -var NextImgDir string var ThumbsDir string 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") flag.StringVar(&ThumbsDir, "thumbsdir", "./images/thumbs/", "Directory where generate thumbs") flag.Parse() @@ -29,22 +27,12 @@ func main() { if htpasswd, err = NewHtpasswd(*htpasswd_file); htpasswd == nil { log.Fatal("Unable to parse htpasswd:", err) } - } else if NextImgDir == "./images/next/" { + } else if *nextImgDir == "./images/next/" { log.Println("Disable admin interface, images will be published without moderation") - NextImgDir = PublishedImgDir + 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) @@ -63,32 +51,38 @@ func main() { http.FileServer(Assets).ServeHTTP(w, r) }) - mux.Handle("/api/", http.StripPrefix("/api", ApiHandler(authFunc))) + pe := &PictureExplorer{ + FileBackend: &LocalFileBackend{"./images/"}, + 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, + 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, + pe.ServeFile(), + pe.GetNextImage, ))) mux.Handle("/images/thumbs/", http.StripPrefix("/images/thumbs", ImagesHandler( "thumbs", 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 }, http.FileServer(http.Dir(ThumbsDir)), - GetNextImage, + pe.GetNextImage, ))) mux.HandleFunc("/admin/", func(w http.ResponseWriter, r *http.Request) { diff --git a/picture.go b/picture.go index 3904cfc..42fb39a 100644 --- a/picture.go +++ b/picture.go @@ -8,16 +8,20 @@ import ( "image/jpeg" _ "image/png" "io" - "io/ioutil" "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 @@ -25,7 +29,7 @@ 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) Swap(i, j int) { a[i], a[j] = a[j], a[i] } @@ -33,137 +37,84 @@ 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(base64.NewDecoder(base64.StdEncoding, blob)); err != nil { - return err - - // Save file - } else if fw, err := os.Create(filepath.Join(NextImgDir, filename+".jpg")); 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 + // Convert to JPEG + img, _, err := image.Decode(base64.NewDecoder(base64.StdEncoding, blob)) + if err != nil { + return err + } + + // Save file + if err := e.PutPicture(e.NextImgDir, filename, &img); err != nil { + return err + } + + 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/version.go b/version.go index 82911a9..f6ffa95 100644 --- a/version.go +++ b/version.go @@ -4,6 +4,8 @@ import ( "io" ) +const VERSION = 0.3 + var ApiVersionRouting = map[string]struct { AuthFunction DispatchFunction @@ -12,7 +14,7 @@ var ApiVersionRouting = map[string]struct { } func showVersion(u *User, args []string, body io.ReadCloser) (interface{}, error) { - m := map[string]interface{}{"version": 0.2} + m := map[string]interface{}{"version": VERSION} if u != nil { m["youare"] = *u