improve file upload #1

This commit is contained in:
Lars Hoogestraat 2018-12-11 18:00:05 +01:00
parent cde4f47f3e
commit 88ddc3973d
9 changed files with 50 additions and 55 deletions

View File

@ -25,21 +25,18 @@ Configuration
### Create user with administration rights ###
Create your first administrator account with create_user:
~~~
./createuser -admin -sqlite path_to_sqlite -username test -email test@example.com -displayname "Hello World" -password secret1234
./createuser -admin -sqlite /path/to/your/sqlite/database -username test -email test@example.com -displayname "Hello World" -password secret1234
~~~
Make sure -admin is set.
TODOs
-----
* Add and fix test
* Database update tasks / simple query builder (v0.3)
* Order possibilities in admin panel (v0.3)
* Revisit Makefile (v0.3)
* Order possibilities in admin panel (v0.4)
* Revisit Makefile (v0.4)
* Review async mails
* Review preview of articles and sites / error handling
* Roles?
Licence
-------

View File

View File

@ -107,6 +107,7 @@ func InitTables(db *sql.DB) error {
"(" +
"id INTEGER PRIMARY KEY, " +
"filename VARCHAR(191) NOT NULL, " +
"unique_name VARCHAR(191) NOT NULL, " +
"size BIGINT NOT NULL, " +
"content_type VARCHAR(150) NOT NULL, " +
"last_modified datetime NOT NULL, " +
@ -114,7 +115,7 @@ func InitTables(db *sql.DB) error {
"CONSTRAINT `fk_file_user` " +
"FOREIGN KEY (user_id) REFERENCES user(id) " +
"ON DELETE CASCADE, " +
"CONSTRAINT file_filename_key UNIQUE (filename) " +
"CONSTRAINT file_unique_name_key UNIQUE (unique_name) " +
");"); err != nil {
return err
}

View File

@ -21,16 +21,16 @@ type FileHandler struct {
//FileGetHandler serves the file based on the url filename
func (fh FileHandler) FileGetHandler(w http.ResponseWriter, r *http.Request) {
rv := getVar(r, "filename")
rv := getVar(r, "uniquename")
f, err := fh.Context.FileService.GetByName(rv, nil)
f, err := fh.Context.FileService.GetByUniqueName(rv, nil)
if err != nil {
http.Error(w, "the file was not found", http.StatusNotFound)
return
}
loc := filepath.Join(fh.Context.ConfigService.Location, f.FullFilename)
loc := filepath.Join(fh.Context.ConfigService.Location, f.UniqueName)
w.Header().Set("Content-Type", f.ContentType)
w.Header().Set("Content-Disposition", "inline")
@ -153,26 +153,13 @@ func AdminUploadFilePostHandler(ctx *middleware.AppContext, w http.ResponseWrite
ct := http.DetectContentType(data)
file := &models.File{
ContentType: ct,
Location: ctx.ConfigService.File.Location,
Author: u,
Size: int64(len(data)),
ContentType: ct,
Location: ctx.ConfigService.File.Location,
Author: u,
Size: int64(len(data)),
FullFilename: h.Filename,
}
var filename string
var ext string
ext = filepath.Ext(h.Filename[1:])
if ext != "" {
filename = h.Filename[0 : len(h.Filename)-len(ext)]
} else {
filename = h.Filename
}
file.Filename = filename
file.Extension = ext
_, err = ctx.FileService.Upload(file, data)
if err != nil {

View File

@ -141,7 +141,7 @@ func main() {
if err != nil {
exitCode = 1
logger.Log.Errorf("failed to start TLS server: %v", err)
logger.Log.Errorf("failed to start server: %v", err)
return
}
}

View File

@ -1,6 +1,7 @@
package models
import (
"errors"
"fmt"
"io/ioutil"
"os"
@ -10,7 +11,6 @@ import (
"git.hoogi.eu/go-blog/components/httperror"
"git.hoogi.eu/go-blog/components/logger"
"git.hoogi.eu/go-blog/utils"
)
//File represents a file
@ -19,6 +19,7 @@ type File struct {
Location string
Filename string
Extension string
UniqueName string
FullFilename string
ContentType string
Size int64
@ -30,7 +31,7 @@ type File struct {
type FileDatasourceService interface {
Create(f *File) (int, error)
Get(fileID int, u *User) (*File, error)
GetByFilename(filename string, u *User) (*File, error)
GetByUniqueName(uniqueName string, u *User) (*File, error)
List(u *User, p *Pagination) ([]File, error)
Count(u *User) (int, error)
Delete(fileID int) error
@ -39,19 +40,19 @@ type FileDatasourceService interface {
// validate validates if mandatory file fields are set
// sanitizes the filename
func (f *File) validate() error {
if len(f.Filename) == 0 {
if len(f.FullFilename) == 0 {
return httperror.ValueRequired("filename")
}
if len(f.Filename) > 255 {
if len(f.FullFilename) > 255 {
return httperror.ValueTooLong("filename", 255)
}
return nil
}
func (f File) buildSanitizedFilename() string {
return utils.SanitizeFilename(f.Filename) + "_" + strconv.Itoa(int(time.Now().Unix())) + f.Extension
func (f File) randomFilename() string {
return strconv.Itoa(int(time.Now().Unix())) + "." + f.Extension
}
//FileService containing the service to interact with files
@ -67,8 +68,8 @@ func (fs FileService) GetByID(fileID int, u *User) (*File, error) {
//GetByName returns the file based on the filename; it the user is given and it is a non admin
//only file specific to this user is returned
func (fs FileService) GetByName(filename string, u *User) (*File, error) {
return fs.Datasource.GetByFilename(filename, u)
func (fs FileService) GetByUniqueName(uniqueName string, u *User) (*File, error) {
return fs.Datasource.GetByUniqueName(uniqueName, u)
}
//List returns a list of files based on the filename; it the user is given and it is a non admin
@ -103,7 +104,7 @@ func (fs FileService) Delete(fileID int, location string, u *User) error {
return err
}
return os.Remove(filepath.Join(location, file.FullFilename))
return os.Remove(filepath.Join(location, file.UniqueName))
}
//Upload uploaded files will be saved at the configured file location, filename is saved in the database
@ -112,9 +113,18 @@ func (fs FileService) Upload(f *File, data []byte) (int, error) {
return -1, err
}
f.FullFilename = f.buildSanitizedFilename()
if len(filepath.Ext(f.FullFilename)) > 0 {
f.Extension = filepath.Ext(f.FullFilename)[1:]
}
fi := filepath.Join(f.Location, f.FullFilename)
if len(f.Extension) == 0 {
//?
return -1, errors.New("not a valid file extension")
}
f.UniqueName = f.randomFilename()
fi := filepath.Join(f.Location, f.UniqueName)
err := ioutil.WriteFile(fi, data, 0640)

View File

@ -13,19 +13,19 @@ type SQLiteFileDatasource struct {
//GetByFilename returns the file based on the filename; it the user is given and it is a non admin
//only file specific to this user is returned
func (rdb SQLiteFileDatasource) GetByFilename(filename string, u *User) (*File, error) {
func (rdb SQLiteFileDatasource) GetByUniqueName(uniqueName string, u *User) (*File, error) {
var stmt bytes.Buffer
var args []interface{}
stmt.WriteString("SELECT f.id, f.filename, f.content_type, f.size, f.last_modified, f.user_id, ")
stmt.WriteString("SELECT f.id, f.filename, f.unique_name, f.content_type, f.size, f.last_modified, f.user_id, ")
stmt.WriteString("u.display_name, u.username, u.email, u.is_admin ")
stmt.WriteString("FROM file as f ")
stmt.WriteString("INNER JOIN user as u ")
stmt.WriteString("ON u.id = f.user_id ")
stmt.WriteString("WHERE f.filename=? ")
stmt.WriteString("WHERE f.unique_name=? ")
args = append(args, filename)
args = append(args, uniqueName)
if u != nil {
if !u.IsAdmin {
@ -37,7 +37,7 @@ func (rdb SQLiteFileDatasource) GetByFilename(filename string, u *User) (*File,
var f File
var ru User
if err := rdb.SQLConn.QueryRow(stmt.String(), args...).Scan(&f.ID, &f.FullFilename, &f.ContentType, &f.Size, &f.LastModified, &ru.ID,
if err := rdb.SQLConn.QueryRow(stmt.String(), args...).Scan(&f.ID, &f.FullFilename, &f.UniqueName, &f.ContentType, &f.Size, &f.LastModified, &ru.ID,
&ru.DisplayName, &ru.Username, &ru.Email, &ru.IsAdmin); err != nil {
return nil, err
}
@ -54,7 +54,7 @@ func (rdb SQLiteFileDatasource) Get(fileID int, u *User) (*File, error) {
var args []interface{}
stmt.WriteString("SELECT f.id, f.filename, f.content_type, f.size, f.last_modified, f.user_id, ")
stmt.WriteString("SELECT f.id, f.filename, f.unique_name, f.content_type, f.size, f.last_modified, f.user_id, ")
stmt.WriteString("u.display_name, u.username, u.email, u.is_admin ")
stmt.WriteString("FROM file as f ")
stmt.WriteString("INNER JOIN user as u ")
@ -73,7 +73,7 @@ func (rdb SQLiteFileDatasource) Get(fileID int, u *User) (*File, error) {
var f File
var ru User
if err := rdb.SQLConn.QueryRow(stmt.String(), args...).Scan(&f.ID, &f.FullFilename, &f.ContentType, &f.Size, &f.LastModified, &ru.ID,
if err := rdb.SQLConn.QueryRow(stmt.String(), args...).Scan(&f.ID, &f.FullFilename, &f.UniqueName, &f.ContentType, &f.Size, &f.LastModified, &ru.ID,
&ru.DisplayName, &ru.Username, &ru.Email, &ru.IsAdmin); err != nil {
return nil, err
}
@ -85,8 +85,8 @@ func (rdb SQLiteFileDatasource) Get(fileID int, u *User) (*File, error) {
//Create inserts some file meta information into the database
func (rdb SQLiteFileDatasource) Create(f *File) (int, error) {
res, err := rdb.SQLConn.Exec("INSERT INTO file (filename, content_type, size, last_modified, user_id) VALUES(?, ?, ?, ?, ?)",
f.FullFilename, f.ContentType, f.Size, time.Now(), f.Author.ID)
res, err := rdb.SQLConn.Exec("INSERT INTO file (filename, unique_name, content_type, size, last_modified, user_id) VALUES(?, ?, ?, ?, ?, ?)",
f.FullFilename, f.UniqueName, f.ContentType, f.Size, time.Now(), f.Author.ID)
if err != nil {
return -1, err
@ -108,7 +108,7 @@ func (rdb SQLiteFileDatasource) List(u *User, p *Pagination) ([]File, error) {
var args []interface{}
stmt.WriteString("SELECT f.id, f.filename, f.content_type, f.size, f.last_modified, ")
stmt.WriteString("SELECT f.id, f.filename, f.unique_name, f.content_type, f.size, f.last_modified, ")
stmt.WriteString("u.id, u.display_name, u.username, u.email, u.is_admin ")
stmt.WriteString("FROM file as f ")
stmt.WriteString("INNER JOIN user as u ")
@ -142,7 +142,7 @@ func (rdb SQLiteFileDatasource) List(u *User, p *Pagination) ([]File, error) {
var us User
for rows.Next() {
if err = rows.Scan(&f.ID, &f.FullFilename, &f.ContentType, &f.Size, &f.LastModified, &us.ID, &us.DisplayName,
if err = rows.Scan(&f.ID, &f.FullFilename, &f.UniqueName, &f.ContentType, &f.Size, &f.LastModified, &us.ID, &us.DisplayName,
&us.Username, &us.Email, &u.IsAdmin); err != nil {
return nil, err
}

View File

@ -165,7 +165,7 @@ func publicRoutes(ctx *m.AppContext, router *mux.Router, chain alice.Chain) {
router.Handle("/site/{site}", chain.Then(useTemplateHandler(ctx, c.GetSiteHandler))).Methods("GET")
router.Handle("/file/{filename}", chain.ThenFunc(fh.FileGetHandler)).Methods("GET")
router.Handle("/file/{uniquename}", chain.ThenFunc(fh.FileGetHandler)).Methods("GET")
router.Handle("/admin", chain.Then(useTemplateHandler(ctx, c.LoginHandler))).Methods("GET")
router.Handle("/admin", chain.Then(useTemplateHandler(ctx, c.LoginPostHandler))).Methods("POST")

View File

@ -25,12 +25,12 @@
{{range .files}}
<tr>
<td>{{.LastModified | FormatDateTime}}</td>
<td>{{ApplicationURL}}/file/{{.FullFilename}}</td>
<td>{{ApplicationURL}}/file/{{.UniqueName}}</td>
<td>{{.ContentType}}</td>
<td>{{.Size}}</td>
<td>{{.Author.Username}}</td>
<td class="action-data">
<a href="/file/{{.FullFilename}}" title="Show file">Show file</a>
<a href="/file/{{.UniqueName}}" title="Show file">Show file</a>
<a href="/admin/file/delete/{{.ID}}" title="Remove">Remove</a>
</td>
</tr>