Photoblog: Refactoring

This commit is contained in:
Meutel 2017-07-29 19:22:11 +02:00
parent 8ec59e4826
commit b8f03b87c9
5 changed files with 163 additions and 93 deletions

View File

@ -15,6 +15,14 @@ import (
"github.com/gorilla/sessions"
)
const (
USER = "username"
PWD = "password"
SESS_USER = "username"
SESSION = "photoblog.session"
)
// Application
type AuthCookie struct {
Templates *template.Template
Store *sessions.CookieStore
@ -22,37 +30,23 @@ type AuthCookie struct {
PasswordSecret string
}
func (app *AuthCookie) LoginPage(res http.ResponseWriter, req *http.Request) {
formErr := make(map[string]error)
switch req.Method {
case "POST":
username := req.FormValue("username")
formErr["username"] = app.VerifyUsername(username)
formErr["password"] = app.VerifyPassword(username, req.FormValue("password"), res, req)
if formErr["username"] == nil && formErr["password"] == nil {
app.SaveUsername(username, res, req)
RedirectHome(res, req)
return
}
fallthrough
case "GET":
app.Templates.ExecuteTemplate(res, "login.html", formErr)
}
}
func (app *AuthCookie) LogoutPage(res http.ResponseWriter, req *http.Request) {
app.SaveUsername("", res, req)
RedirectHome(res, req)
}
// Verify Username
func (app *AuthCookie) VerifyUsername(username string) error {
if username == "" {
return errors.New("Empty username")
}
return nil
}
// Verify password for user, check with file
func (app *AuthCookie) VerifyPassword(username, password string, res http.ResponseWriter, req *http.Request) error {
if password == "" {
return errors.New("Empty password")
}
if username == "" {
return nil
}
// open password file
passfile, err := os.Open(filepath.Join(app.DataDir.Name(), username, ".password"))
if err != nil {
log.Println("Cannot open password file", err)
@ -60,14 +54,13 @@ func (app *AuthCookie) VerifyPassword(username, password string, res http.Respon
}
defer passfile.Close()
if username == "" {
return nil
}
// read password file
expected, err := ioutil.ReadAll(passfile)
if err != nil {
log.Println("Cannot read password file", err)
return errors.New("Authentification failed")
}
// hash password and compare
expectedStr := string(expected)
var expectedMAC []byte
fmt.Sscanf(expectedStr, "%x", &expectedMAC)
@ -78,29 +71,67 @@ func (app *AuthCookie) VerifyPassword(username, password string, res http.Respon
log.Printf("Unmatched password for %s: %x - %x\n", username, passwordMAC, expectedMAC)
return errors.New("Authentification failed")
}
log.Printf("Authentification successful")
log.Printf("Authentification successful (" + username + ")")
return nil
}
func (app *AuthCookie) CurrentSession(res http.ResponseWriter, req *http.Request) *sessions.Session {
session, _ := app.Store.Get(req, "session")
return session
}
// Save username in session
func (app *AuthCookie) SaveUsername(username string, res http.ResponseWriter, req *http.Request) {
session := app.CurrentSession(res, req)
session.Values["username"] = username
session.Values[SESS_USER] = username
session.Save(req, res)
}
// Test if a user is logged in
func (app *AuthCookie) IsLoggedIn(res http.ResponseWriter, req *http.Request) bool {
session := app.CurrentSession(res, req)
return session != nil && session.Values["username"] != ""
return session != nil && session.Values[SESS_USER] != ""
}
// Current username (from session cookie)
func (app *AuthCookie) Username(res http.ResponseWriter, req *http.Request) string {
session := app.CurrentSession(res, req)
if session == nil {
return ""
}
return session.Values["username"].(string)
val := session.Values[SESS_USER]
if val == nil {
return ""
}
func RedirectHome(res http.ResponseWriter, req *http.Request) {
return val.(string)
}
// Current session
func (app *AuthCookie) CurrentSession(res http.ResponseWriter, req *http.Request) *sessions.Session {
session, _ := app.Store.Get(req, SESSION)
return session
}
// Redirect to home page
func (app *AuthCookie) RedirectHome(res http.ResponseWriter, req *http.Request) {
http.Redirect(res, req, "/", http.StatusSeeOther)
}
// ROUTES //
// login: form
func (app *AuthCookie) LoginPage(res http.ResponseWriter, req *http.Request) {
formErr := make(map[string]error)
if req.Method == http.MethodPost {
username := req.FormValue(USER)
formErr[USER] = app.VerifyUsername(username)
formErr[PWD] = app.VerifyPassword(username, req.FormValue(PWD), res, req)
if formErr[USER] == nil && formErr[PWD] == nil {
app.SaveUsername(username, res, req)
app.RedirectHome(res, req)
return
}
}
app.Templates.ExecuteTemplate(res, "login.html", formErr)
}
// logout: delete session and redirect to home
func (app *AuthCookie) LogoutPage(res http.ResponseWriter, req *http.Request) {
app.SaveUsername("", res, req)
app.RedirectHome(res, req)
}

View File

@ -28,19 +28,19 @@ func main() {
}
defer data.Close()
// FIXME config file
app := photo.PhotoBlog{
admin.AuthCookie{Templates: tpl,
admin.AuthCookie{
Templates: tpl,
Store: sessions.NewCookieStore([]byte("flQ6QzM/c3Jtdl9ycDx6OXRIfFgK")),
DataDir: data,
PasswordSecret: "d2xnNSwoREQhfSxBVDQ0bF0yb2AK",
},
}
fileServer := http.FileServer(http.Dir(data.Name()))
http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.RequestURI, "/data") {
http.StripPrefix("/data/", fileServer).ServeHTTP(res, req)
http.StripPrefix("/data/", http.FileServer(http.Dir(data.Name()))).ServeHTTP(res, req)
} else {
app.HomePage(res, req)
}

View File

@ -1,6 +1,7 @@
package photo
import (
"errors"
"fmt"
"io"
"net/http"
@ -19,21 +20,58 @@ const (
var (
PHOTOEXT = [3]string{".jpg", ".jpeg", ".png"}
PHOTOMIME = [2]string{"image/png", "image/jpeg"}
)
type AppData struct {
// Application
type PhotoBlog struct {
admin.AuthCookie
}
// Generic model
type PhotoModel struct {
Username string
Photos []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,
}
}
// File and date of last modification
type TimedFile struct {
Mod time.Time
Path string
}
type timeMap []TimedFile
// 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)
@ -45,14 +83,7 @@ func (m timeMap) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
type PhotoBlog struct {
admin.AuthCookie
}
func RedirectHome(res http.ResponseWriter, req *http.Request) {
// FIXME duplicate admin
http.Redirect(res, req, "/", http.StatusSeeOther)
}
// test if file is a photo (by extension)
func IsPhoto(path string) bool {
for _, ext := range PHOTOEXT {
if strings.HasSuffix(path, ext) {
@ -62,6 +93,17 @@ func IsPhoto(path string) bool {
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)
@ -74,60 +116,57 @@ func Latest(photos timeMap) []string {
return latest
}
func (app *PhotoBlog) HomePage(res http.ResponseWriter, req *http.Request) {
data := AppData{
Username: app.Username(res, req),
}
photos := make([]TimedFile, 0)
filepath.Walk(app.DataDir.Name(), func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && IsPhoto(path) {
photos = append(photos, TimedFile{
info.ModTime(),
path,
})
}
return nil
})
data.Photos = Latest(photos)
app.Templates.ExecuteTemplate(res, "home.html", data)
}
func (app *PhotoBlog) UploadPage(res http.ResponseWriter, req *http.Request) {
if !app.IsLoggedIn(res, req) {
RedirectHome(res, req)
return
}
data := AppData{
Username: app.Username(res, req),
}
switch req.Method {
case "POST":
message, err := app.NewPhoto(res, req)
if err != nil {
data.Err = err
}
data.Message = message
fallthrough
case "GET":
app.Templates.ExecuteTemplate(res, "upload.html", data)
}
}
func (app *PhotoBlog) NewPhoto(res http.ResponseWriter, req *http.Request) (string, error) {
// 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()
// FIXME check type
tmpFile, err := os.Create(filepath.Join(app.DataDir.Name(), app.Username(res, req), header.Filename))
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 tmpFile.Close()
defer target.Close()
sz, err := io.Copy(tmpFile, file)
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 {
if !info.IsDir() && IsPhoto(path) {
photos = append(photos, NewTimedFile(info.ModTime(), path))
}
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 := NewPhotoModel(app.Username(res, 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)
}

View File

@ -4,7 +4,7 @@
<body>
<header>
<ul>
<li><p>Home</p></li>
<li><a href="/">Home</a></li>
{{ if .Username }}
<li><p>Hello {{ .Username }}</p></li>
<li style="float: right"><a href="/logout">Logout</a></li>

View File

@ -12,7 +12,7 @@
<input type="password" name="password" placeholder="password...">
</label>
<br>
<input type="submit">
<input type="submit" value="Login">
<a href="/">Back</a>
</form>
</div>