File uploads

This commit is contained in:
Magnus Åhall 2023-06-22 06:52:27 +02:00
parent 5319492760
commit 910a7a15c7
7 changed files with 105 additions and 36 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
notes notes
upload

29
file.go
View File

@ -12,12 +12,12 @@ import (
type File struct { type File struct {
ID int ID int
UserID int `db:"user_id"` UserID int `db:"user_id"`
NodeID int
Filename string Filename string
Size int64 Size int64
MIME string MIME string
MD5 string MD5 string
Uploaded time.Time Uploaded time.Time
} }
func (session Session) AddFile(file *File) (err error) { // {{{ func (session Session) AddFile(file *File) (err error) { // {{{
@ -25,11 +25,12 @@ func (session Session) AddFile(file *File) (err error) {// {{{
var rows *sqlx.Rows var rows *sqlx.Rows
rows, err = db.Queryx(` rows, err = db.Queryx(`
INSERT INTO file(user_id, filename, size, mime, md5) INSERT INTO file(user_id, node_id, filename, size, mime, md5)
VALUES($1, $2, $3, $4, $5) VALUES($1, $2, $3, $4, $5, $6)
RETURNING id RETURNING id
`, `,
file.UserID, file.UserID,
file.NodeID,
file.Filename, file.Filename,
file.Size, file.Size,
file.MIME, file.MIME,
@ -45,5 +46,27 @@ func (session Session) AddFile(file *File) (err error) {// {{{
fmt.Printf("%#v\n", file) fmt.Printf("%#v\n", file)
return return
} // }}} } // }}}
func (session Session) Files(nodeID int) (files []File, err error) { // {{{
var rows *sqlx.Rows
rows, err = db.Queryx(
`SELECT * FROM files WHERE user_id = $1 AND node_id = $2`,
session.UserID,
nodeID,
)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
file := File{}
if err = rows.StructScan(&file); err != nil {
return
}
files = append(files, file)
}
return
} // }}}
// vim: foldmethod=marker // vim: foldmethod=marker

54
main.go
View File

@ -15,13 +15,14 @@ import (
"path" "path"
"regexp" "regexp"
"strings" "strings"
"strconv"
"time" "time"
_ "embed" _ "embed"
) )
const VERSION = "v0.1.2"; const VERSION = "v0.1.2";
const LISTEN_HOST = "0.0.0.0"; const LISTEN_HOST = "0.0.0.0";
const DB_SCHEMA = 4 const DB_SCHEMA = 5
var ( var (
flagPort int flagPort int
@ -358,9 +359,9 @@ func nodeDelete(w http.ResponseWriter, r *http.Request) {// {{{
}) })
}// }}} }// }}}
func nodeUpload(w http.ResponseWriter, r *http.Request) {// {{{ func nodeUpload(w http.ResponseWriter, r *http.Request) {// {{{
log.Println("/node/upload")
var err error var err error
var session Session var session Session
log.Println("/node/upload")
if session, _, err = ValidateSession(r, true); err != nil { if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err) responseError(w, err)
@ -369,7 +370,8 @@ func nodeUpload(w http.ResponseWriter, r *http.Request) {// {{{
// Parse our multipart form, 10 << 20 specifies a maximum // Parse our multipart form, 10 << 20 specifies a maximum
// upload of 10 MB files. // upload of 10 MB files.
r.ParseMultipartForm(100 << 20) r.Body = http.MaxBytesReader(w, r.Body, 128<<20+512)
r.ParseMultipartForm(128 << 20)
// FormFile returns the first file for the given key `myFile` // FormFile returns the first file for the given key `myFile`
// it also returns the FileHeader so we can get the Filename, // it also returns the FileHeader so we can get the Filename,
@ -381,9 +383,8 @@ func nodeUpload(w http.ResponseWriter, r *http.Request) {// {{{
} }
defer file.Close() defer file.Close()
// Read metadata of file for database, // Read metadata of file for database, and also file contents
// and also file contents for MD5, which is used // for MD5, which is used to store the file on disk.
// to store the file on disk.
fileBytes, err := io.ReadAll(file) fileBytes, err := io.ReadAll(file)
if err != nil { if err != nil {
responseError(w, err) responseError(w, err)
@ -392,7 +393,14 @@ func nodeUpload(w http.ResponseWriter, r *http.Request) {// {{{
md5sumBytes := md5.Sum(fileBytes) md5sumBytes := md5.Sum(fileBytes)
md5sum := hex.EncodeToString(md5sumBytes[:]) md5sum := hex.EncodeToString(md5sumBytes[:])
var nodeID int
if nodeID, err = strconv.Atoi(r.PostFormValue("NodeID")); err != nil {
responseError(w, err)
return
}
nodeFile := File{ nodeFile := File{
NodeID: nodeID,
Filename: handler.Filename, Filename: handler.Filename,
Size: handler.Size, Size: handler.Size,
MIME: handler.Header.Get("Content-Type"), MIME: handler.Header.Get("Content-Type"),
@ -403,6 +411,10 @@ func nodeUpload(w http.ResponseWriter, r *http.Request) {// {{{
return return
} }
// Files are stored in a directory structure composed of
// the first three characters in the md5sum, which is statistically
// distributed by design, making sure there aren't too many files in
// a single directory.
path := filepath.Join( path := filepath.Join(
config.Application.Directories.Upload, config.Application.Directories.Upload,
md5sum[0:1], md5sum[0:1],
@ -428,6 +440,36 @@ func nodeUpload(w http.ResponseWriter, r *http.Request) {// {{{
responseData(w, map[string]interface{}{ responseData(w, map[string]interface{}{
"OK": true, "OK": true,
"File": nodeFile,
})
}// }}}
func nodeFiles(w http.ResponseWriter, r *http.Request) {// {{{
var err error
var session Session
var files []File
if session, _, err = ValidateSession(r, true); err != nil {
responseError(w, err)
return
}
req := struct {
NodeID int
}{}
if err = parseRequest(r, &req); err != nil {
responseError(w, err)
return
}
files, err = session.Files(req.NodeID)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]interface{}{
"OK": true,
"Files": files,
}) })
}// }}} }// }}}

2
sql/0005.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE public.file ADD node_id int4 NOT NULL;
ALTER TABLE public.file ADD CONSTRAINT file_node_fk FOREIGN KEY (node_id) REFERENCES public.node(id) ON DELETE RESTRICT ON UPDATE RESTRICT;

View File

@ -34,7 +34,6 @@ h1 {
background: rgba(0, 0, 0, 0.35); background: rgba(0, 0, 0, 0.35);
} }
#menu { #menu {
display: none;
position: absolute; position: absolute;
top: 24px; top: 24px;
right: 24px; right: 24px;
@ -44,9 +43,6 @@ h1 {
box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.5); box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.5);
z-index: 1025; z-index: 1025;
} }
#menu.show {
display: initial;
}
#menu .item { #menu .item {
padding: 16px; padding: 16px;
border-bottom: 1px solid #aaa; border-bottom: 1px solid #aaa;

View File

@ -52,11 +52,11 @@ export class NodeUI extends Component {
let upload = ''; let upload = '';
if(this.upload.value) if(this.upload.value)
upload = html`<${UploadUI} app=${this} />` upload = html`<${UploadUI} nodeui=${this} />`
let menu = ''; let menu = '';
if(this.menu.value) if(this.menu.value)
upload = html`<${Menu} app=${this} />` upload = html`<${Menu} nodeui=${this} />`
return html` return html`
${menu} ${menu}
@ -234,6 +234,9 @@ class NodeContent extends Component {
}//}}} }//}}}
} }
class NodeFiles extends Component {
}
class Node { class Node {
constructor(app, nodeID) {//{{{ constructor(app, nodeID) {//{{{
this.app = app this.app = app
@ -261,14 +264,14 @@ class Node {
} }
class Menu extends Component { class Menu extends Component {
render({ app }) {//{{{ render({ nodeui }) {//{{{
return html` return html`
<div id="blackout" onclick=${()=>app.menu.value = false}></div> <div id="blackout" onclick=${()=>nodeui.menu.value = false}></div>
<div id="menu" class="${app.menu.value ? 'show' : ''}"> <div id="menu">
<div class="item" onclick=${()=>app.renameNode()}>Rename</div> <div class="item" onclick=${()=>{ nodeui.renameNode(); nodeui.menu.value = false }}>Rename</div>
<div class="item separator" onclick=${()=>app.deleteNode()}>Delete</div> <div class="item separator" onclick=${()=>{ nodeui.deleteNode(); nodeui.menu.value = false }}>Delete</div>
<div class="item separator" onclick=${()=>{ app.upload.value = true; app.menu.value = false }}>Upload</div> <div class="item separator" onclick=${()=>{ nodeui.upload.value = true; nodeui.menu.value = false }}>Upload</div>
<div class="item" onclick=${()=>app.logout()}>Log out</div> <div class="item" onclick=${()=>{ nodeui.logout(); nodeui.menu.value = false }}>Log out</div>
</div> </div>
` `
}//}}} }//}}}
@ -303,6 +306,7 @@ class UploadUI extends Component {
}//}}} }//}}}
upload() {//{{{ upload() {//{{{
let nodeID = this.props.nodeui.node.value.ID
this.fileRefs = [] this.fileRefs = []
this.progressRefs = [] this.progressRefs = []
@ -311,17 +315,23 @@ class UploadUI extends Component {
for(let i = 0; i < input.files.length; i++) { for(let i = 0; i < input.files.length; i++) {
this.fileRefs.push(createRef()) this.fileRefs.push(createRef())
this.progressRefs.push(createRef()) this.progressRefs.push(createRef())
this.postFile(input.files[i], progress=>{
this.postFile(
input.files[i],
nodeID,
progress=>{
this.progressRefs[i].current.innerHTML = `${progress}%` this.progressRefs[i].current.innerHTML = `${progress}%`
}, ()=>{ },
()=>{
this.fileRefs[i].current.classList.add("done") this.fileRefs[i].current.classList.add("done")
this.progressRefs[i].current.classList.add("done") this.progressRefs[i].current.classList.add("done")
}) })
} }
}//}}} }//}}}
postFile(file, progressCallback, doneCallback) {//{{{ postFile(file, nodeID, progressCallback, doneCallback) {//{{{
var formdata = new FormData() var formdata = new FormData()
formdata.append('file', file) formdata.append('file', file)
formdata.append('NodeID', nodeID)
var request = new XMLHttpRequest() var request = new XMLHttpRequest()

View File

@ -41,7 +41,6 @@ h1 {
} }
#menu { #menu {
display: none;
position: absolute; position: absolute;
top: 24px; top: 24px;
right: 24px; right: 24px;
@ -51,10 +50,6 @@ h1 {
box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.5); box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.5);
z-index: 1025; z-index: 1025;
&.show {
display: initial;
}
.item { .item {
padding: 16px; padding: 16px;
border-bottom: 1px solid #aaa; border-bottom: 1px solid #aaa;