File management from within Logseq, proof-of-concept

Table of Contents

  • Introduction
  • Screenshot
  • Current Features
  • Code

Introduction

UPDATE: This has been updated to build on top of File explorer from within Logseq

This is motivated by @alex0’s description #2 about reusing Logseq’s outliner for file management. This implementation is:

  • Different on purpose:
    • Forget most conveniences and prepare to learn a different workflow.
    • It tries to blend with Logseq’s experience.
      • Logseq is different and this is too.
  • Experimental:
    • No specific direction, other than proving the concept
    • Highly UNTESTED
      • If you want quality, use instead the file manager of your operating system.
  • Missing important features. Here are only a few:
    • No consideration for access rights
    • No check for duplicate block entries
    • NO UNDO
    • Almost no safety:
      • Not easy to make a mess, but certainly possible
        • Use at own risk
      • It is technically a SECURITY RISK
        • Don’t use it in production

Screenshot

image


Current Features

  • Every feature of File explorer from within Logseq
  • Safely click on any button to see if that path currently exists in the file system.
    • Simple clicks make no changes.
    • Alt + click proceeds to changes without confirmation.
      • Basic info is provided as messages and console logs.
  • Alt + click on button Delete to delete both the underlying file/folder and block.
    • Deleting manually a block or file/folder doesn’t update its pair.
    • Subfolders and sub-blocks get also deleted.
  • Alt + click on button Create to create a missing folder and all of its missing ancestors (if any) recursively.
    • This doesn’t create files.

Code

  • Follow the steps at File explorer from within Logseq
  • At the end of file preload.js, normally inside ...\Logseq\app-0.9.13\resources\app\js , replace the exposure of getfs with the full set of functions like this:
    contextBridge.exposeInMainWorld('getfs', ()=>fs )
    
    • The above is the security risk.
  • Optional additional styles inside file custom.css:
    button.filesystem.create {
      background: #ffee00;
    }
    button.filesystem.delete {
      background: #ff4400;
    }
    
  • Inside page FileSystem in Logseq, lower than the existing code-block, add the following code in a separate javascript code-block:
const LS = logseq.api
const Module = logseq.Module
const Block = Module.Block

function statusMsg(status, msg){
    Module.Msg.ofStatus(msg, status)
}
const error = statusMsg.bind(null, "error")
const info = statusMsg.bind(null, "info")
const success = statusMsg.bind(null, "success")

const FS = Module.FileSystem
.setStatic(function appendButtonsForBlock(div, block){
    const blockId = block.uuid
    if (block.properties.foldername) div.append(
        FS.button("Create", FS.onCreateClicked, blockId),
        FS.button("Read", FS.onReadClicked, blockId)
    )
    div.append( FS.button("Delete", FS.onDeleteClicked, blockId) )
})
.setStatic(function create(block, cb){
    const parent = Block.parentOf(block)
    if (!parent) return cb("exists")

    create(parent, (res)=>{
        if (res !== "done" && res !== "exists") return cb(res)

        const path = FS.fullPathOfBlock(block)
        fs.access(path, (err)=>{
            if (!err) return cb("exists")

            fs.mkdir(path, (err)=>{
                if (!err) console.log("CREATED FOLDER: " + path)
                cb(err ? err.message : "done")
            })
        })
    })
})
.setStatic(function onCreateClicked(blockId, e){
    const block = LS.get_block(blockId)
    fs.access(FS.fullPathOfBlock(block), (err)=>{
        if (!err) return info("exists")

        if (e.altKey) FS.create(block, FS.onCreated)
        else info("Alt to create")
    })
})
.setStatic(function onCreated(res){
    if (res === "done") success("created")
    else if (res === "exists") error("root should exist") // special
    else error(res)
})
.setStatic(function onDeleteClicked(blockId, e){
    const block = LS.get_block(blockId)
    const fullPath = FS.fullPathOfBlock(block)
    fs.access(fullPath, (err)=>{
        if (err) return info("doesn't exist")

        if (e.altKey) FS.unlink(fullPath, block)
        else info("Alt to delete")
    })
})
.setStatic(function onUnlinkFinished(type, path, block, err){
    if (err) return error(FS.nameOfErr(err) + ": " + path)

    console.log("DELETED " + type + ": " + path)
    success("deleted")
    LS.remove_block(block.uuid)
    console.log("DELETED BLOCK: " + FS.nameOfBlock(block))
})
.setStatic(function unlink(path, block){
    if (FS.isFolder(path)) {
        const cb = FS.onUnlinkFinished.bind(null, "FOLDER", path, block)
        fs.rmdir(path, {recursive: true, force: true}, cb)
    } else {
        const cb = FS.onUnlinkFinished.bind(null, "FILE", path, block)
        fs.unlink(path, cb)
    }
})

const fs = FS.fs