Files
plainleaf/libraries/Library/Std/Infrastructure/Share.md
T

4.5 KiB

#meta

Implementation

share = {}

local shareSchema = {
  type = "object",
  properties = {
    uri = schema.string(),
    hash = schema.string(),
    mode = schema.string(), -- push | pull | sync
  },
}

-- Content change detection hash of 8 characters
-- First strips out share frontmatter-related keys to avoid hash-induced change detections
function share.contentHash(content)
  content = share.cleanFrontmatter(content)
  return crypto.sha256(content):sub(1, 8)
end

function share.cleanFrontmatter(text)
  return index.patchFrontmatter(text, {
    {op="delete-key", path="share.uri"},
    {op="delete-key", path="share.hash"},
    {op="delete-key", path="share.mode"},
  })
end

function share.setFrontmatter(m, text)
  return index.patchFrontmatter(text, {
    {op="set-key", path="share.uri", value=m.uri},
    {op="set-key", path="share.hash", value=m.hash},
    {op="set-key", path="share.mode", value=m.mode}
  })
end


-- Returns true if actually performed share, false if it was a no-op
function share.sharePage(name)
  local updateCurrent = not name
  local text
  if updateCurrent then
    text = editor.getText()
    name = editor.getCurrentPage()
  else
    text = space.readPage(name)
  end
  
  local m = index.extractFrontmatter(text).frontmatter.share
  if not m or type(m) != "table" then
    error("Not configured")
  end

  print("Current share frontmatter", m)
  
  local remoteText = net.readURI(m.uri, {encoding="text/markdown"})
  if not remoteText then
    error("Could not read " .. m.uri)
  end

  local mode = m.mode or "push"
  local oldHash = m.hash
  local newLocalHash = share.contentHash(text)
  local newHash = newLocalHash

  if mode == "sync" then
    -- Two-way sync mode
    local newRemoteHash = share.contentHash(remoteText)
    -- Sync cases
    if oldHash == newLocalHash and oldHash == newRemoteHash then
      print("Both sides up to date: nothing to do")
      return false
    elseif oldHash != newLocalHash and oldHash == newRemoteHash then
      print("Local changes, not remote one: local -> remote")
      net.writeURI(m.uri, share.cleanFrontmatter(text))
    elseif oldHash == newLocalHash and oldHash != newRemoteHash then
      print("Remote changes, not local ones: remote -> local")
      text = remoteText
      newHash = newRemoteHash
    else
      print("Changes on both ends: conflict")
      error("Conflict: not implemented yet")
    end
  elseif mode == "push" then
    if oldHash == newLocalHash then
      print("No local changes: nothing to do")
      return false
    else
      print("Local changes in push mode: local -> remote")
      net.writeURI(m.uri, share.cleanFrontmatter(text))
    end 
  elseif mode == "pull" then
    local newRemoteHash = share.contentHash(remoteText)
    if oldHash == newRemoteHash then
      print("No remote changes: nothing to do")
      return false
    else
      if oldHash != newLocalHash then
        -- Local changes made since last pull, warn user
        if not editor.confirm("Local changes made to " .. name .. " are you ok to overwrite them with updated remote content from " .. m.uri .. "?") then
          return false
        end
      end  
      print("Remote changes in pull mode: remote -> local")
      text = remoteText
      newHash = newRemoteHash
    end
  end

  -- Update frontmatter
  m.hash = newHash
  m.mode = mode
  local updatedText = share.setFrontmatter(m, text)
  if updateCurrent then
    editor.setText(updatedText)
  else
    space.writePage(name, updatedText)
  end
  return true
end

command.define {
  name = "Share: Page",
  key = "Ctrl-p",
  mac = "Cmd-p",
  run = function()
    local ok, ret = pcall(share.sharePage)
    if ok then
      if ret then
        editor.flashNotification "Share successful"
      else
        editor.flashNotification "No changes"
      end
    else -- error
      if ret == "Not configured" then
        local data = {
          name = editor.getCurrentPage(),
          text = editor.getText()
        }
        local shareProviders = service.discover("share:onboard", data)
        local provider = editor.filterBox("Share provider", shareProviders, "Select how you would like to share this page")
        if not provider then
          return
        end
  
        print("Selected", provider)
        local m = service.invoke(provider, data)
        if m then
          editor.setText(share.setFrontmatter(m, data.text))
          editor.flashNotification "Share complete!"
        end
      else
        editor.flashNotification("Error: ".. ret)
      end
    end
  end
}