improve file upload #1
This commit is contained in:
parent
cde4f47f3e
commit
88ddc3973d
11
README.md
11
README.md
|
@ -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
|
||||
-------
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
2
main.go
2
main.go
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue