Fetch remote files when not in indexeddb
This commit is contained in:
parent
f3ca7b9a44
commit
6df2be7944
6 changed files with 153 additions and 36 deletions
42
file.go
42
file.go
|
|
@ -12,13 +12,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
ID int
|
ID int
|
||||||
UUID string
|
UUID string
|
||||||
UserID int `db:"user_id"`
|
UserID int `db:"user_id"`
|
||||||
Filename string
|
Name string `db:"name"`
|
||||||
Size int
|
Size int
|
||||||
MIME string
|
Type string `db:"type"`
|
||||||
Modified time.Time
|
Modified time.Time
|
||||||
|
UploadComplete bool `db:"upload_complete"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -41,9 +42,9 @@ func AddFileToDb(userID int, file *File) (err error) { // {{{
|
||||||
`,
|
`,
|
||||||
file.UserID,
|
file.UserID,
|
||||||
file.UUID,
|
file.UUID,
|
||||||
file.Filename,
|
file.Name,
|
||||||
file.Size,
|
file.Size,
|
||||||
file.MIME,
|
file.Type,
|
||||||
file.Modified,
|
file.Modified,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -94,7 +95,7 @@ func FileIDToPath(id int) (string, string) { // {{{
|
||||||
return dir, fpath
|
return dir, fpath
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
func FileWriteData(userID int, dbID int, data []byte, create bool) (err error) {// {{{
|
func FileWriteData(userID int, dbID int, data []byte, create bool) (err error) { // {{{
|
||||||
dir, fpath := FileIDToPath(dbID)
|
dir, fpath := FileIDToPath(dbID)
|
||||||
err = os.MkdirAll(dir, 0700)
|
err = os.MkdirAll(dir, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -121,6 +122,25 @@ func FileWriteData(userID int, dbID int, data []byte, create bool) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}// }}}
|
} // }}}
|
||||||
|
|
||||||
|
func FileMetadata(userID int, uuid string) (f File, err error) { // {{{
|
||||||
|
row := db.QueryRowx(`SELECT * FROM public.file WHERE user_id = $1 AND "uuid" = $2`, userID, uuid)
|
||||||
|
err = row.StructScan(&f)
|
||||||
|
return
|
||||||
|
|
||||||
|
} // }}}
|
||||||
|
func FileForUser(userID int, uuid string) (f *os.File, md File, err error) { // {{{
|
||||||
|
row := db.QueryRowx(`SELECT * FROM public.file WHERE user_id = $1 AND "uuid" = $2`, userID, uuid)
|
||||||
|
err = row.StructScan(&md)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, fpath := FileIDToPath(md.ID)
|
||||||
|
f, err = os.OpenFile(fpath, os.O_RDONLY, 0600)
|
||||||
|
return
|
||||||
|
|
||||||
|
} // }}}
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
|
|
||||||
42
main.go
42
main.go
|
|
@ -148,6 +148,9 @@ func main() { // {{{
|
||||||
http.HandleFunc("/node/history/retrieve/{uuid}/{offset}", authenticated(actionNodeHistoryRetrieve))
|
http.HandleFunc("/node/history/retrieve/{uuid}/{offset}", authenticated(actionNodeHistoryRetrieve))
|
||||||
http.HandleFunc("/node/history/count/{uuid}", authenticated(actionNodeHistoryCount))
|
http.HandleFunc("/node/history/count/{uuid}", authenticated(actionNodeHistoryCount))
|
||||||
|
|
||||||
|
http.HandleFunc("/file/{uuid}", authenticated(actionFile))
|
||||||
|
http.HandleFunc("/file/{uuid}/metadata", authenticated(actionFileMetadata))
|
||||||
|
|
||||||
http.HandleFunc("/service_worker.js", pageServiceWorker)
|
http.HandleFunc("/service_worker.js", pageServiceWorker)
|
||||||
|
|
||||||
listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port)
|
listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port)
|
||||||
|
|
@ -168,6 +171,11 @@ func authenticated(fn func(http.ResponseWriter, *http.Request)) func(http.Respon
|
||||||
|
|
||||||
// The Bearer token is extracted.
|
// The Bearer token is extracted.
|
||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
|
||||||
|
if authHeader == "" {
|
||||||
|
authHeader = "Bearer " + r.URL.Query().Get("jwt")
|
||||||
|
}
|
||||||
|
|
||||||
authParts := RxpBearerToken.FindStringSubmatch(authHeader)
|
authParts := RxpBearerToken.FindStringSubmatch(authHeader)
|
||||||
if len(authParts) != 2 {
|
if len(authParts) != 2 {
|
||||||
failed(fmt.Errorf("Authorization missing or invalid"))
|
failed(fmt.Errorf("Authorization missing or invalid"))
|
||||||
|
|
@ -404,9 +412,9 @@ func actionSyncFileUpload(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
file := File{}
|
file := File{}
|
||||||
if sentBytes == 0 {
|
if sentBytes == 0 {
|
||||||
file.UUID = uuid
|
file.UUID = uuid
|
||||||
file.Filename = r.Header.Get("X-File-Name")
|
file.Name = r.Header.Get("X-File-Name")
|
||||||
file.Size, _ = strconv.Atoi(r.Header.Get("X-File-Size"))
|
file.Size, _ = strconv.Atoi(r.Header.Get("X-File-Size"))
|
||||||
file.MIME = r.Header.Get("X-File-Type")
|
file.Type = r.Header.Get("X-File-Type")
|
||||||
modded, _ := strconv.ParseInt(r.Header.Get("X-File-Modified"), 10, 64)
|
modded, _ := strconv.ParseInt(r.Header.Get("X-File-Modified"), 10, 64)
|
||||||
file.Modified = time.UnixMilli(modded)
|
file.Modified = time.UnixMilli(modded)
|
||||||
err := AddFileToDb(session.UserID, &file)
|
err := AddFileToDb(session.UserID, &file)
|
||||||
|
|
@ -492,6 +500,36 @@ func actionUserSetPreferences(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
})
|
})
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
|
func actionFile(w http.ResponseWriter, r *http.Request) {// {{{
|
||||||
|
uuid := r.PathValue("uuid")
|
||||||
|
Log.Info("FOO", "uuid", uuid)
|
||||||
|
session := getUserSession(r)
|
||||||
|
f, md, err := FileForUser(session.UserID, uuid)
|
||||||
|
if err != nil {
|
||||||
|
Log.Error("file_metadata", "error", err)
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeContent(w, r, md.Name, md.Modified, f)
|
||||||
|
}// }}}
|
||||||
|
func actionFileMetadata(w http.ResponseWriter, r *http.Request) {// {{{
|
||||||
|
uuid := r.PathValue("uuid")
|
||||||
|
session := getUserSession(r)
|
||||||
|
md, err := FileMetadata(session.UserID, uuid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
Log.Error("file_metadata", "error", err)
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]any{
|
||||||
|
"OK": true,
|
||||||
|
"Metadata": md,
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
|
||||||
func createNewUser(username string) { // {{{
|
func createNewUser(username string) { // {{{
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { CustomHTMLElement } from "./lib/custom_html_element.mjs";
|
import { CustomHTMLElement } from "./lib/custom_html_element.mjs";
|
||||||
|
import { API } from 'api'
|
||||||
|
|
||||||
export class N2File extends CustomHTMLElement {
|
export class N2File extends CustomHTMLElement {
|
||||||
static {// {{{
|
static {// {{{
|
||||||
|
|
@ -34,6 +35,9 @@ export class N2File extends CustomHTMLElement {
|
||||||
constructor() {// {{{
|
constructor() {// {{{
|
||||||
super(true)
|
super(true)
|
||||||
|
|
||||||
|
this.file = null
|
||||||
|
this.uuid = null
|
||||||
|
|
||||||
this.addEventListener('click', event => {
|
this.addEventListener('click', event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
@ -45,16 +49,34 @@ export class N2File extends CustomHTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open in browser.
|
// Open in browser.
|
||||||
window.open(
|
this.openInBrowser(event)
|
||||||
URL.createObjectURL(this.file),
|
|
||||||
event.ctrlKey ? '_blank' : '_self',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.render()
|
this.render()
|
||||||
}// }}}
|
}// }}}
|
||||||
|
openInBrowser(event) {
|
||||||
|
// this.file is only defined when file is stored in IndexedDB.
|
||||||
|
if (this.file)
|
||||||
|
window.open(
|
||||||
|
URL.createObjectURL(this.file),
|
||||||
|
event.ctrlKey ? '_blank' : '_self',
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
const jwt = localStorage.getItem('token')
|
||||||
|
window.open(
|
||||||
|
|
||||||
|
`/file/${this.uuid}?jwt=${jwt}`,
|
||||||
|
event.ctrlKey ? '_blank' : '_self',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
async downloadToDisk() {// {{{
|
async downloadToDisk() {// {{{
|
||||||
try {
|
try {
|
||||||
|
// this.file is only defined when file is stored in IndexedDB.
|
||||||
|
// TODO: implement download of remote content.
|
||||||
|
if (this.file === null)
|
||||||
|
throw new Error('This file is only available online. Download not implemented yet.')
|
||||||
|
|
||||||
const handle = await window.showSaveFilePicker({
|
const handle = await window.showSaveFilePicker({
|
||||||
suggestedName: this.file.name,
|
suggestedName: this.file.name,
|
||||||
})
|
})
|
||||||
|
|
@ -78,26 +100,61 @@ export class N2File extends CustomHTMLElement {
|
||||||
// while Marked lib has to be returned a string when exiting this function.
|
// while Marked lib has to be returned a string when exiting this function.
|
||||||
// populateImg makes sure this returned img element exists and then populates it
|
// populateImg makes sure this returned img element exists and then populates it
|
||||||
// with the image from IndexedDB.
|
// with the image from IndexedDB.
|
||||||
const file = await globalThis.nodeStore.files.get(src.slice(5))
|
const fileUUID = src.slice(5)
|
||||||
if (!file)
|
this.uuid = fileUUID
|
||||||
return
|
const file = await globalThis.nodeStore.files.get(fileUUID)
|
||||||
this.file = file.file
|
|
||||||
|
|
||||||
if (file.file.type.startsWith('image/'))
|
if (file) {
|
||||||
this.elImage.src = URL.createObjectURL(file.file)
|
this.renderLocalFile(file)
|
||||||
else {
|
} else {
|
||||||
// Check for and use an existing MIME type icon.
|
/* <file> can be undefined when a node is retrieved but files aren't synced.
|
||||||
// Place them in static/images/file_icons/ and replace the slash with an underscore.
|
* Try to show from server.
|
||||||
const url = `/images/${_VERSION}/file_icons/${file.file.type.replaceAll('/', '_')}.svg`
|
*
|
||||||
const res = await fetch(url)
|
* Metadata is needed to know whether to display an image or the file representation. */
|
||||||
if (res.ok)
|
try {
|
||||||
this.elImage.src = url
|
const res = await API.query('GET', `/file/${fileUUID}/metadata`)
|
||||||
|
this.renderRemoteFile(res.Metadata)
|
||||||
this.elFilename.innerText = file.file.name
|
} catch (err) {
|
||||||
this.elFilename.style.display = 'block'
|
console.error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else
|
} else
|
||||||
|
// Probably an external URL.
|
||||||
this.elImage.src = src
|
this.elImage.src = src
|
||||||
}// }}}
|
}// }}}
|
||||||
|
async renderLocalFile(file) {// {{{
|
||||||
|
this.file = file.file
|
||||||
|
|
||||||
|
if (file.file.type.startsWith('image/'))
|
||||||
|
this.elImage.src = URL.createObjectURL(file.file)
|
||||||
|
else {
|
||||||
|
// Check for and use an existing MIME type icon.
|
||||||
|
// Place them in static/images/file_icons/ and replace the slash with an underscore.
|
||||||
|
const url = `/images/${_VERSION}/file_icons/${file.file.type.replaceAll('/', '_')}.svg`
|
||||||
|
const res = await fetch(url)
|
||||||
|
if (res.ok)
|
||||||
|
this.elImage.src = url
|
||||||
|
|
||||||
|
this.elFilename.innerText = file.file.name
|
||||||
|
this.elFilename.style.display = 'block'
|
||||||
|
}
|
||||||
|
}// }}}
|
||||||
|
async renderRemoteFile(md) {// {{{
|
||||||
|
if (md.Type.startsWith('image/')) {
|
||||||
|
const jwt = localStorage.getItem('token')
|
||||||
|
this.elImage.src = `/file/${md.UUID}?jwt=${jwt}`
|
||||||
|
} else {
|
||||||
|
// Check for and use an existing MIME type icon.
|
||||||
|
// Place them in static/images/file_icons/ and replace the slash with an underscore.
|
||||||
|
const url = `/images/${_VERSION}/file_icons/${md.Type.replaceAll('/', '_')}.svg`
|
||||||
|
const res = await fetch(url)
|
||||||
|
if (res.ok)
|
||||||
|
this.elImage.src = url
|
||||||
|
|
||||||
|
this.elFilename.innerText = md.Name
|
||||||
|
this.elFilename.style.display = 'block'
|
||||||
|
}
|
||||||
|
}// }}}
|
||||||
}
|
}
|
||||||
customElements.define('n2-file', N2File)
|
customElements.define('n2-file', N2File)
|
||||||
|
|
|
||||||
|
|
@ -597,7 +597,6 @@ class FileNodesLinkStore extends SimpleNodeStore {
|
||||||
// Maintains an index to quickly find orphaned files.
|
// Maintains an index to quickly find orphaned files.
|
||||||
nodelink.NodeCount = nodelink.Nodes.length
|
nodelink.NodeCount = nodelink.Nodes.length
|
||||||
|
|
||||||
console.log('removing', nodelink)
|
|
||||||
await this.add({data: nodelink})
|
await this.add({data: nodelink})
|
||||||
}
|
}
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
@ -620,7 +619,6 @@ class FileNodesLinkStore extends SimpleNodeStore {
|
||||||
// Maintains an index to quickly find orphaned files.
|
// Maintains an index to quickly find orphaned files.
|
||||||
nodelink.NodeCount = nodelink.Nodes.length
|
nodelink.NodeCount = nodelink.Nodes.length
|
||||||
|
|
||||||
console.log('pushing', nodelink)
|
|
||||||
await this.add({data: nodelink})
|
await this.add({data: nodelink})
|
||||||
}
|
}
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
|
||||||
|
|
@ -549,7 +549,6 @@ export class Node {
|
||||||
if (this.UUID == ROOT_NODE)
|
if (this.UUID == ROOT_NODE)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
// File links from the original, unmodified content are found to compare to links from the new content.
|
// File links from the original, unmodified content are found to compare to links from the new content.
|
||||||
// All files (images) needs to be indexed to nodes in order to track orphaned files.
|
// All files (images) needs to be indexed to nodes in order to track orphaned files.
|
||||||
const before = new Set([...this.data.Content.matchAll(dblink)].map(m => m[1]))
|
const before = new Set([...this.data.Content.matchAll(dblink)].map(m => m[1]))
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,20 @@ const CACHED_ASSETS = [
|
||||||
|
|
||||||
'/images/{{ .VERSION }}/collapsed.svg',
|
'/images/{{ .VERSION }}/collapsed.svg',
|
||||||
'/images/{{ .VERSION }}/expanded.svg',
|
'/images/{{ .VERSION }}/expanded.svg',
|
||||||
|
'/images/{{ .VERSION }}/icon_back.svg',
|
||||||
'/images/{{ .VERSION }}/icon_history.svg',
|
'/images/{{ .VERSION }}/icon_history.svg',
|
||||||
'/images/{{ .VERSION }}/icon_home.svg',
|
'/images/{{ .VERSION }}/icon_home.svg',
|
||||||
'/images/{{ .VERSION }}/icon_markdown_hollow.svg',
|
'/images/{{ .VERSION }}/icon_markdown_hollow.svg',
|
||||||
'/images/{{ .VERSION }}/icon_markdown.svg',
|
'/images/{{ .VERSION }}/icon_markdown.svg',
|
||||||
|
'/images/{{ .VERSION }}/icon_menu.svg',
|
||||||
|
'/images/{{ .VERSION }}/icon_new_document.svg',
|
||||||
'/images/{{ .VERSION }}/icon_refresh.svg',
|
'/images/{{ .VERSION }}/icon_refresh.svg',
|
||||||
'/images/{{ .VERSION }}/icon_save_disabled.svg',
|
'/images/{{ .VERSION }}/icon_save_disabled.svg',
|
||||||
'/images/{{ .VERSION }}/icon_save.svg',
|
'/images/{{ .VERSION }}/icon_save.svg',
|
||||||
'/images/{{ .VERSION }}/icon_search.svg',
|
'/images/{{ .VERSION }}/icon_search.svg',
|
||||||
'/images/{{ .VERSION }}/icon_settings.svg',
|
'/images/{{ .VERSION }}/icon_settings.svg',
|
||||||
'/images/{{ .VERSION }}/icon_table.svg',
|
'/images/{{ .VERSION }}/icon_table.svg',
|
||||||
|
'/images/{{ .VERSION }}/icon_transfer.svg',
|
||||||
'/images/{{ .VERSION }}/leaf.svg',
|
'/images/{{ .VERSION }}/leaf.svg',
|
||||||
'/images/{{ .VERSION }}/logo_small.svg',
|
'/images/{{ .VERSION }}/logo_small.svg',
|
||||||
'/images/{{ .VERSION }}/logo.svg',
|
'/images/{{ .VERSION }}/logo.svg',
|
||||||
|
|
@ -42,6 +46,7 @@ const CACHED_ASSETS = [
|
||||||
'/js/{{ .VERSION }}/notes2.mjs',
|
'/js/{{ .VERSION }}/notes2.mjs',
|
||||||
'/js/{{ .VERSION }}/page_history.mjs',
|
'/js/{{ .VERSION }}/page_history.mjs',
|
||||||
'/js/{{ .VERSION }}/page_node.mjs',
|
'/js/{{ .VERSION }}/page_node.mjs',
|
||||||
|
'/js/{{ .VERSION }}/page_preferences.mjs',
|
||||||
'/js/{{ .VERSION }}/page_storage.mjs',
|
'/js/{{ .VERSION }}/page_storage.mjs',
|
||||||
'/js/{{ .VERSION }}/sidebar.mjs',
|
'/js/{{ .VERSION }}/sidebar.mjs',
|
||||||
'/js/{{ .VERSION }}/sync.mjs',
|
'/js/{{ .VERSION }}/sync.mjs',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue