package photo import ( "errors" "fmt" "io" "net/http" "os" "path/filepath" "sort" "strings" "time" "github.com/gorilla/csrf" "meutel.net/meutel/go-examples/photoblog/admin" ) const ( DISPLAY_SIZE = 5 // number of photos displayed on home ) var ( PHOTOEXT = [3]string{".jpg", ".jpeg", ".png"} PHOTOMIME = [2]string{"image/png", "image/jpeg"} DATA = "/data/" ) // Application type PhotoBlog struct { admin.AuthCookie } // Generic model type PhotoModel struct { Username string Err error Message string } // Constructor PhotoModel func NewPhotoModel(username string) *PhotoModel { return &PhotoModel{ Username: username, } } // Home page model type PhotoHomeModel struct { PhotoModel Photos []string } // Constructor PhotoHomeModel func NewPhotoHomeModel(username string, photos []string) *PhotoHomeModel { return &PhotoHomeModel{ PhotoModel: *NewPhotoModel(username), Photos: photos, } } // Upload page model type PhotoUploadModel struct { PhotoModel CsrfToken string } // Constructor PhotoUploadModel func NewPhotoUploadModel(username, tok string) *PhotoUploadModel { return &PhotoUploadModel{ PhotoModel: *NewPhotoModel(username), CsrfToken: tok, } } // File and date of last modification type TimedFile struct { Mod time.Time Path string } // Constructor TimedFile func NewTimedFile(mod time.Time, path string) *TimedFile { return &TimedFile{ mod, path, } } // TimedFile sort type timeMap []*TimedFile func (m timeMap) Len() int { return len(m) } func (m timeMap) Less(i, j int) bool { return m[i].Mod.After(m[j].Mod) } func (m timeMap) Swap(i, j int) { m[i], m[j] = m[j], m[i] } // test if file is a photo (by extension) func IsPhoto(path string) bool { for _, ext := range PHOTOEXT { if strings.HasSuffix(path, ext) { return true } } return false } // test if mime type is a photo func IsPhotoMime(mime string) bool { for _, accepted := range PHOTOMIME { if mime == accepted { return true } } return false } // get Latest files func Latest(photos timeMap) []string { latest := make([]string, 0, DISPLAY_SIZE) sort.Sort(photos) for i, v := range photos { if i > DISPLAY_SIZE { break } latest = append(latest, v.Path) } return latest } // Add new uploaded photo func (app *PhotoBlog) AddPhoto(res http.ResponseWriter, req *http.Request) (string, error) { file, header, err := req.FormFile("file") if err != nil { return "", err } defer file.Close() mime := header.Header.Get("Content-Type") if !IsPhotoMime(mime) { return "", errors.New("This file type is not accepted (" + mime + ")") } target, err := os.Create(filepath.Join(app.DataDir.Name(), app.Username(res, req), header.Filename)) if err != nil { return "", err } defer target.Close() sz, err := io.Copy(target, file) if err != nil { return "", err } return fmt.Sprintf("File uploaded (%d bytes)", sz), nil } // ROUTES // // Home: list latest photos func (app *PhotoBlog) HomePage(res http.ResponseWriter, req *http.Request) { photos := make([]*TimedFile, 0) filepath.Walk(app.DataDir.Name(), func(path string, info os.FileInfo, err error) error { relPath, err := filepath.Rel(app.DataDir.Name(), path) if !info.IsDir() && IsPhoto(path) && err == nil { photos = append(photos, NewTimedFile(info.ModTime(), DATA+relPath)) } return nil }) app.Templates.ExecuteTemplate(res, "home.html", NewPhotoHomeModel(app.Username(res, req), Latest(photos))) } // Upload: form upload photo func (app *PhotoBlog) UploadPage(res http.ResponseWriter, req *http.Request) { if !app.IsLoggedIn(res, req) { app.RedirectHome(res, req) return } data := NewPhotoUploadModel(app.Username(res, req), csrf.Token(req)) if req.Method == http.MethodPost { message, err := app.AddPhoto(res, req) data.Err = err data.Message = message } app.Templates.ExecuteTemplate(res, "upload.html", data) }