Explorar el Código

added notes-app demo with 3 days of progress. focus: nginx dav, extjs 2.0.2 app

Harlan J. Iverson hace 1 año
padre
commit
853d460d72

+ 72 - 0
notes-app/README.md

@@ -0,0 +1,72 @@
+# Notes App
+
+This is a notes web app that uses WebDAV to store plaintext files that can be
+synced to a remote server.
+
+The reason for storing the notes on the filesystem and exposing via WebDAV is to
+make editing with any editor possible.
+
+It's a precursor that lead to [The Notes App](https://harlanji.gumroad.com/l/the-notes-app).
+
+## Requirements
+
+* ExtJS 2.0.2 stored in or linked from the ext-2.0.2 path of each day.
+* Nginx with the DAV extension enabled
+* An .htpasswd file with at least one user confured in it (via apache tools).
+* Optionally lsyncd
+
+I believe the password in the provided .htpasswd files is demo123, pardon if I'm mistaken.
+
+## Usage
+
+The script `setup-deb-bullseye.sh` can be used to install a site to nginx,
+or followed for nginx installation on any other platform.
+
+The `sync-notes` script can be used to run lsync, manually or via a cron job.
+
+## Dev Practice
+
+This is an older demo from when I was just establishing the habit of breaking 
+practice into a daily format.
+
+It focuses on:
+
+* Making minimal code changes from a prior demo
+* Getting an app running in Nginx 
+* Nginx DAV storage
+* Syncing info to a remote server with LSyncd
+
+The code is messy and intentionally left so because it's derived from an earlier 
+work, which I mean to create a minimal diff from and sequentially commit to Git.
+Please pardon the apparently amateur style. Releasing as-is to the dev-practice repo.
+
+## Changelog
+
+### 2023-05-21
+
+Updated README for release in harlanji/dev-practice repo.
+
+### Day 3
+
+* Added Lunr code to index in browser and/or nodejs given notes
+
+TODO
+
+* Save notes as paintext and load them as plaintext (this would enable easy import).
+* Create a way to change storage location for cross-site
+* Either mount the webdav FS for node to index remote notes
+* Or create an iterator over notes in the store to download all content.
+
+
+
+### 2022-01-16
+
+* Added cross-origin support to nginx config
+* Updated code to work cross origin
+
+### 2022-01-15
+
+* Created baseline notes demo
+* Created nginx config with webdav
+* Created install script
+* Created Lsyncd config

+ 1 - 0
notes-app/day-1/.htpasswd

@@ -0,0 +1 @@
+demo:$apr1$DZ.vncRb$7TkCdcpB5LV4h1nQI6flh.

+ 28 - 0
notes-app/day-1/index.html

@@ -0,0 +1,28 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Notes</title>
+    <link rel="stylesheet" href="ext-2.0.2/resources/css/ext-all.css">
+
+    <script src="ext-2.0.2/adapter/jquery/jquery.js"></script>
+    <script src="ext-2.0.2/adapter/jquery/ext-jquery-adapter.js"></script>
+
+    <script src="ext-2.0.2/ext-all-debug.js"></script>
+    <script>
+    var viewport = new Ext.Viewport({id: 'viewport', layout: 'fit'});
+    </script>
+    <script src="notes.js"></script>
+  </head>
+  <body>
+
+<div id="notes-app">
+
+</div>
+
+<script>
+runNotesApp();
+</script>
+
+  </body>
+</html>

+ 7 - 0
notes-app/day-1/lsyncd-notes.config

@@ -0,0 +1,7 @@
+sync {
+  default.rsyncssh,
+  source = "/home/hi/p/cityapper-com-school/demos/notes-app/day-1/storage",
+  host   = "hi@cityapper.com",
+  targetdir = "/home/hi/storage/notes",
+  delete = false
+}

+ 52 - 0
notes-app/day-1/nginx.conf

@@ -0,0 +1,52 @@
+server {
+	listen 8109;
+	listen [::]:8109;
+
+	set $app_root '/home/hi/p/cityapper-com-school/demos/notes-app/day-1';
+
+	root $app_root;
+        auth_basic_user_file $app_root/.htpasswd;
+        auth_basic           "Restricted";
+
+
+
+	# Add index.php to the list if you are using PHP
+	index index.html;
+
+	#server_name hi-pi-64.local;
+
+	location / {
+		# First attempt to serve request as file, then
+		# as directory, then fall back to displaying a 404.
+		try_files $uri $uri/ =404;
+	}
+
+
+        location /storage {
+          autoindex on;
+          alias $app_root/storage;
+
+
+    # dav allowed method
+    dav_methods     PUT DELETE MKCOL COPY MOVE;
+    # Allow current scope perform specified DAV method
+    dav_ext_methods PROPFIND OPTIONS;
+
+    
+    # In this folder, newly created folder or file is to have specified permission. If none is given, default is user:rw. If all or group permission is specified, user could be skipped
+    dav_access      user:rw group:rw all:r;
+
+    # Temporary folder
+    client_body_temp_path   /tmp;
+    
+    # MAX size of uploaded file, 0 mean unlimited
+    client_max_body_size    0;
+    
+    # Allow autocreate folder here if necessary
+    create_full_put_path    on;
+
+
+
+
+        }
+}

+ 250 - 0
notes-app/day-1/notes.js

@@ -0,0 +1,250 @@
+
+// Returns a function, that, as long as it continues to be invoked, will not
+// be triggered. The function will be called after it stops being called for
+// N milliseconds. If `immediate` is passed, trigger the function on the
+// leading edge, instead of the trailing.
+function debounce(func, wait, immediate) {
+        var timeout;
+        return function() {
+                var context = this, args = arguments;
+                var later = function() {
+                        timeout = null;
+                        if (!immediate) func.apply(context, args);
+                };
+                var callNow = immediate && !timeout;
+                clearTimeout(timeout);
+                timeout = setTimeout(later, wait);
+                if (callNow) func.apply(context, args);
+        };
+};
+
+
+function WebDavFileSystem(baseUrl) {
+  this.baseUrl = baseUrl || "/storage";
+}
+
+WebDavFileSystem.prototype.list = function list(path) {
+  var baseUrl = this.baseUrl;
+
+  var url = baseUrl + path;
+
+
+
+  return fetch(url, {method: "PROPFIND"})
+    .then(function (r) { return r.text(); })
+    .then(function (str) { return new window.DOMParser().parseFromString(str, "text/xml"); })
+    .then(function (r) {
+      console.log(r);
+      var items = r.querySelectorAll("multistatus response");
+      items = Array.prototype.slice.call(items);
+      items = items.map(function (i) {
+        var i = {
+          path: i.querySelector("href").textContent,
+          created_at: i.querySelector("prop creationdate").textContent,
+          modified_at: i.querySelector("prop getlastmodified").textContent,
+          length_bytes: i.querySelector("prop getcontentlength").textContent,
+          display_name: i.querySelector("prop displayname").textContent,
+          status: i.querySelector("status").textContent
+        };
+
+        if (i.path) {
+          i.path = i.path.substr(baseUrl.length);
+        }
+
+        return i;
+      });
+
+
+
+      return items;
+    });
+
+
+}
+
+WebDavFileSystem.prototype.upload = function upload(path, contentType, content) {
+  var url = this.baseUrl + path;
+  return fetch(url, {method: 'PUT', body: content});
+}
+
+WebDavFileSystem.prototype.download = function download(path) {
+  var url = this.baseUrl + path;
+  return fetch(url)
+    .then(function (r) { return r.text(); })
+}
+
+
+var fs = new WebDavFileSystem();
+
+var notesStore;
+
+
+function saveNote() {
+  // post note to /upload
+  // post new .csv to /upload (logic should be on server)
+}
+
+function syncNotesFromDir() {
+  fs.list("/").then(function (files) {
+    var IDs = files.filter(function (f) {
+      return f.path.endsWith(".txt");
+    });
+
+    var data = IDs.map(function (id) {
+      return [id.path, id.created_at, id.modified_at];
+    })
+    console.log(data);
+    //notesStore.clear();
+    notesStore.loadData(data, true);
+  });
+}
+var notesStore = new Ext.data.SimpleStore({
+  fields: ["id", "created", "modified", "title", "body", "attachment_keys"],
+  id: 0,
+  data: []
+});
+notesStore.on("add", function (store, records, idx) {
+  console.log("Notes added: ");
+  console.log(records);
+  var noteNodes = records.map(noteToNode);
+  var notesFolders = Ext.ComponentMgr.get("notesFolders");
+  var notesNode = notesFolders.root;
+  notesNode.appendChild(noteNodes);
+});
+
+function noteToNode(record) {
+  var title = record.data.id;
+  var node = new Ext.tree.TreeNode({
+    text: title,
+    leaf: true,
+    noteId: record.data.id
+  });
+  return node;
+}
+
+
+
+function  runNotesApp () {
+    Ext.QuickTips.init();
+    var viewport = Ext.getCmp('viewport');
+
+    var win;
+    var selectedNoteId = null;
+    if (!win) {
+      var noteNodes = notesStore.getRange().map(noteToNode);
+      var notesNode = new Ext.tree.TreeNode({
+        text: "Notes",
+        expanded: true
+      });
+      notesNode.appendChild(noteNodes);
+
+      function addHandler() {
+        var newId = "/note-" + Math.random() + ".txt";
+        var date = new Date().toString();
+        newId = prompt("ID for new note:", newId);
+        // this should be the effect of a dispatch.
+        fs.upload(newId, "text/plain", "").then(function () {
+
+          notesStore.loadData([
+            [newId, date, date]
+          ], true);
+          var note = notesStore.getById(newId);
+          //notesNode.appendChild(noteToNode(note));
+        });
+      }
+      var foldersComp = {
+        xtype: "treepanel",
+        id: "notesFolders",
+        tbar: [{
+          text: 'Add',
+          handler: addHandler
+        }],
+        root: notesNode,
+        listeners: {
+          click: function (node, evt) {
+            var id = node.attributes.noteId;
+            var editor = Ext.ComponentMgr.get("noteEditor");
+            selectedNoteId = id; // includes deselect, eg. folder.
+            if (!id) {
+              editor.setValue("");
+              return;
+            }
+            console.log("note click: " + id);
+            var note = notesStore.getById(id);
+            //var noteHtml = note.data.body;
+            fs.download( note.data.id ).catch(function (err) {
+              console.log("error: " + err.httpStatus);
+            }).then(function (noteHtml) {
+              var editor = Ext.ComponentMgr.get("noteEditor");
+              editor.setValue(noteHtml);
+            });
+          }
+        }
+      };
+      var foldersConfig = {
+        region: "west",
+        split: true,
+        width: 160,
+        layout: "fit",
+        border: false,
+        items: [foldersComp]
+      };
+      var noteConfig = {
+        region: "center",
+        split: true,
+        border: false,
+        layout: "border",
+        items: [{
+          region: "center",
+          xtype: "htmleditor",
+          html: "Note.",
+          id: "noteEditor",
+          border: false
+        }, {
+          region: "south",
+          border: false,
+          html: "Note stats."
+        }]
+      };
+      var searchConfig = {
+        xtype: "textfield",
+        width: 150,
+        emptyText: "Search..."
+      };
+      win = new Ext.Panel({
+        tbar: [{
+          xtype: "tbspacer"
+        }, searchConfig],
+        id: "notes-win",
+        header: false,
+        title: "Notes",
+        layout: "border",
+        bodyBorder: false,
+        autoShow: true,
+        //border: false,
+        //height: 280,
+        //width: 460,
+        items: [foldersConfig, noteConfig],
+        //bbar: []
+      });
+    }
+
+    viewport.add(win);
+    viewport.doLayout();
+    //win.show();
+    //win.maximize();
+
+    syncNotesFromDir();
+    var editor = Ext.ComponentMgr.get("noteEditor");
+    editor.on("sync", debounce(function (e, html) {
+      console.log("Editor sync. Selected = " + selectedNoteId);
+      if (!selectedNoteId) {
+        return;
+      }
+      var note = notesStore.getById(selectedNoteId);
+      fs.upload(note.data.id, "text/plain", html);
+      //note.beginEdit();
+      //note.set("body", html);
+      //note.endEdit();
+    }, 750));
+}

+ 18 - 0
notes-app/day-1/scripts/setup-deb-bullseye.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+set -e;
+
+mkdir -p storage
+
+sudo chgrp www-data storage \
+                   .htpasswd \
+                   nginx.conf
+
+sudo chmod 770 storage
+sudo chmod g+s storage
+
+sudo chmod 640 .htpasswd nginx.conf
+
+sudo ln -sf $PWD/nginx.conf /etc/nginx/sites-enabled/notes-app-d1.conf
+
+sudo nginx -t
+sudo service nginx reload

+ 3 - 0
notes-app/day-1/scripts/sync-notes

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+lsyncd -nodaemon lsyncd-notes.config

+ 1 - 0
notes-app/day-1/storage/note-0.07840108117769362.txt

@@ -0,0 +1 @@
+<div>&nbsp;jfdsdsf</div><div><br></div><div><br></div><div>fdsf</div><div><br></div><div>fdsfs<br></div>

+ 0 - 0
notes-app/day-1/storage/note-0.5922552422053498.txt


+ 1 - 0
notes-app/day-2/.htpasswd

@@ -0,0 +1 @@
+demo:$apr1$DZ.vncRb$7TkCdcpB5LV4h1nQI6flh.

+ 28 - 0
notes-app/day-2/index.html

@@ -0,0 +1,28 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Notes</title>
+    <link rel="stylesheet" href="ext-2.0.2/resources/css/ext-all.css">
+
+    <script src="ext-2.0.2/adapter/jquery/jquery.js"></script>
+    <script src="ext-2.0.2/adapter/jquery/ext-jquery-adapter.js"></script>
+
+    <script src="ext-2.0.2/ext-all-debug.js"></script>
+    <script>
+    var viewport = new Ext.Viewport({id: 'viewport', layout: 'fit'});
+    </script>
+    <script src="notes.js"></script>
+  </head>
+  <body>
+
+<div id="notes-app">
+
+</div>
+
+<script>
+runNotesApp();
+</script>
+
+  </body>
+</html>

+ 7 - 0
notes-app/day-2/lsyncd-notes.config

@@ -0,0 +1,7 @@
+sync {
+  default.rsyncssh,
+  source = "/home/hi/p/cityapper-com-school/demos/notes-app/day-1/storage",
+  host   = "hi@cityapper.com",
+  targetdir = "/home/hi/storage/notes",
+  delete = false
+}

+ 66 - 0
notes-app/day-2/nginx.conf

@@ -0,0 +1,66 @@
+server {
+	listen 8109;
+	listen [::]:8109;
+
+	set $app_root '/home/hi/p/cityapper-com-school/demos/notes-app/day-2';
+
+	root $app_root;
+        auth_basic_user_file $app_root/.htpasswd;
+        auth_basic           "Restricted";
+
+
+
+	# Add index.php to the list if you are using PHP
+	index index.html;
+
+	#server_name hi-pi-64.local;
+
+	location / {
+		# First attempt to serve request as file, then
+		# as directory, then fall back to displaying a 404.
+		try_files $uri $uri/ =404;
+	}
+
+
+        location /storage {
+          autoindex on;
+          alias $app_root/storage;
+
+    # Preflighted requests
+    if ($request_method = OPTIONS) {
+      add_header "Access-Control-Allow-Origin" *;
+      add_header "Access-Control-Allow-Methods" "GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK, PROPFIND";
+      add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept, DNT, X-CustomHeader, Keep-Alive,User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Range, Range, Depth";
+      return 200;
+    }
+
+    if ($request_method = (GET|POST|HEAD|DELETE|PROPFIND)) {
+      add_header "Access-Control-Allow-Origin" *;
+      add_header "Access-Control-Allow-Methods" "GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK, PROPFIND";
+      add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
+    }
+
+
+    # dav allowed method
+    dav_methods     PUT DELETE MKCOL COPY MOVE;
+    # Allow current scope perform specified DAV method
+    dav_ext_methods PROPFIND OPTIONS;
+
+    
+    # In this folder, newly created folder or file is to have specified permission. If none is given, default is user:rw. If all or group permission is specified, user could be skipped
+    dav_access      user:rw group:rw all:r;
+
+    # Temporary folder
+    client_body_temp_path   /tmp;
+    
+    # MAX size of uploaded file, 0 mean unlimited
+    client_max_body_size    0;
+    
+    # Allow autocreate folder here if necessary
+    create_full_put_path    on;
+
+
+
+
+        }
+}

+ 250 - 0
notes-app/day-2/notes.js

@@ -0,0 +1,250 @@
+// Returns a function, that, as long as it continues to be invoked, will not
+// be triggered. The function will be called after it stops being called for
+// N milliseconds. If `immediate` is passed, trigger the function on the
+// leading edge, instead of the trailing.
+function debounce(func, wait, immediate) {
+        var timeout;
+        return function() {
+                var context = this, args = arguments;
+                var later = function() {
+                        timeout = null;
+                        if (!immediate) func.apply(context, args);
+                };
+                var callNow = immediate && !timeout;
+                clearTimeout(timeout);
+                timeout = setTimeout(later, wait);
+                if (callNow) func.apply(context, args);
+        };
+};
+
+
+function WebDavFileSystem(baseUrl) {
+  this.baseUrl = baseUrl || "/storage";
+}
+
+WebDavFileSystem.prototype.list = function list(path) {
+  var baseUrl = this.baseUrl;
+
+  var url = baseUrl + path;
+
+
+
+  return fetch(url, {method: "PROPFIND", credentials: 'include',  mode: 'cors'})
+    .then(function (r) { return r.text(); })
+    .then(function (str) { return new window.DOMParser().parseFromString(str, "text/xml"); })
+    .then(function (r) {
+      console.log(r);
+      var items = r.querySelectorAll("multistatus response");
+      items = Array.prototype.slice.call(items);
+      items = items.map(function (i) {
+        var i = {
+          path: i.querySelector("href").textContent,
+          created_at: i.querySelector("prop creationdate").textContent,
+          modified_at: i.querySelector("prop getlastmodified").textContent,
+          length_bytes: i.querySelector("prop getcontentlength").textContent,
+          display_name: i.querySelector("prop displayname").textContent,
+          status: i.querySelector("status").textContent
+        };
+
+        if (i.path) {
+          var baseUrlParts = new URL(baseUrl, document.baseURI);
+          i.path = i.path.substr(baseUrlParts.pathname.length);
+        }
+
+        return i;
+      });
+
+
+
+      return items;
+    });
+
+
+}
+
+WebDavFileSystem.prototype.upload = function upload(path, contentType, content) {
+  var url = this.baseUrl + path;
+  return fetch(url, {method: 'PUT', body: content, credentials: 'include',  mode: 'cors'});
+}
+
+WebDavFileSystem.prototype.download = function download(path) {
+  var url = this.baseUrl + path;
+  return fetch(url, {credentials: 'include',  mode: 'cors'})
+    .then(function (r) { return r.text(); })
+}
+
+
+var fs = new WebDavFileSystem();
+
+var notesStore;
+
+
+function saveNote() {
+  // post note to /upload
+  // post new .csv to /upload (logic should be on server)
+}
+
+function syncNotesFromDir() {
+  fs.list("/").then(function (files) {
+    var IDs = files.filter(function (f) {
+      return f.path.endsWith(".txt");
+    });
+
+    var data = IDs.map(function (id) {
+      return [id.path, id.created_at, id.modified_at];
+    })
+    console.log(data);
+    //notesStore.clear();
+    notesStore.loadData(data, true);
+  });
+}
+var notesStore = new Ext.data.SimpleStore({
+  fields: ["id", "created", "modified", "title", "body", "attachment_keys"],
+  id: 0,
+  data: []
+});
+notesStore.on("add", function (store, records, idx) {
+  console.log("Notes added: ");
+  console.log(records);
+  var noteNodes = records.map(noteToNode);
+  var notesFolders = Ext.ComponentMgr.get("notesFolders");
+  var notesNode = notesFolders.root;
+  notesNode.appendChild(noteNodes);
+});
+
+function noteToNode(record) {
+  var title = record.data.id;
+  var node = new Ext.tree.TreeNode({
+    text: title,
+    leaf: true,
+    noteId: record.data.id
+  });
+  return node;
+}
+
+
+
+function  runNotesApp () {
+    Ext.QuickTips.init();
+    var viewport = Ext.getCmp('viewport');
+
+    var win;
+    var selectedNoteId = null;
+    if (!win) {
+      var noteNodes = notesStore.getRange().map(noteToNode);
+      var notesNode = new Ext.tree.TreeNode({
+        text: "Notes",
+        expanded: true
+      });
+      notesNode.appendChild(noteNodes);
+
+      function addHandler() {
+        var newId = "/note-" + Math.random() + ".txt";
+        var date = new Date().toString();
+        newId = prompt("ID for new note:", newId);
+        // this should be the effect of a dispatch.
+        fs.upload(newId, "text/plain", "").then(function () {
+
+          notesStore.loadData([
+            [newId, date, date]
+          ], true);
+          var note = notesStore.getById(newId);
+          //notesNode.appendChild(noteToNode(note));
+        });
+      }
+      var foldersComp = {
+        xtype: "treepanel",
+        id: "notesFolders",
+        tbar: [{
+          text: 'Add',
+          handler: addHandler
+        }],
+        root: notesNode,
+        listeners: {
+          click: function (node, evt) {
+            var id = node.attributes.noteId;
+            var editor = Ext.ComponentMgr.get("noteEditor");
+            selectedNoteId = id; // includes deselect, eg. folder.
+            if (!id) {
+              editor.setValue("");
+              return;
+            }
+            console.log("note click: " + id);
+            var note = notesStore.getById(id);
+            //var noteHtml = note.data.body;
+            fs.download( note.data.id ).catch(function (err) {
+              console.log("error: " + err.httpStatus);
+            }).then(function (noteHtml) {
+              var editor = Ext.ComponentMgr.get("noteEditor");
+              editor.setValue(noteHtml);
+            });
+          }
+        }
+      };
+      var foldersConfig = {
+        region: "west",
+        split: true,
+        width: 160,
+        layout: "fit",
+        border: false,
+        items: [foldersComp]
+      };
+      var noteConfig = {
+        region: "center",
+        split: true,
+        border: false,
+        layout: "border",
+        items: [{
+          region: "center",
+          xtype: "htmleditor",
+          html: "Note.",
+          id: "noteEditor",
+          border: false
+        }, {
+          region: "south",
+          border: false,
+          html: "Note stats."
+        }]
+      };
+      var searchConfig = {
+        xtype: "textfield",
+        width: 150,
+        emptyText: "Search..."
+      };
+      win = new Ext.Panel({
+        tbar: [{
+          xtype: "tbspacer"
+        }, searchConfig],
+        id: "notes-win",
+        header: false,
+        title: "Notes",
+        layout: "border",
+        bodyBorder: false,
+        autoShow: true,
+        //border: false,
+        //height: 280,
+        //width: 460,
+        items: [foldersConfig, noteConfig],
+        //bbar: []
+      });
+    }
+
+    viewport.add(win);
+    viewport.doLayout();
+    //win.show();
+    //win.maximize();
+
+    syncNotesFromDir();
+    var editor = Ext.ComponentMgr.get("noteEditor");
+    editor.on("sync", debounce(function (e, html) {
+      console.log("Editor sync. Selected = " + selectedNoteId);
+      if (!selectedNoteId) {
+        return;
+      }
+      var note = notesStore.getById(selectedNoteId);
+      fs.upload(note.data.id, "text/plain", html);
+      //note.beginEdit();
+      //note.set("body", html);
+      //note.endEdit();
+    }, 750));
+}

+ 18 - 0
notes-app/day-2/scripts/setup-deb-bullseye.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+set -e;
+
+mkdir -p storage
+
+sudo chgrp www-data storage \
+                   .htpasswd \
+                   nginx.conf
+
+sudo chmod 770 storage
+sudo chmod g+s storage
+
+sudo chmod 640 .htpasswd nginx.conf
+
+sudo ln -sf $PWD/nginx.conf /etc/nginx/sites-enabled/notes-app.conf
+
+sudo nginx -t
+sudo service nginx reload

+ 3 - 0
notes-app/day-2/scripts/sync-notes

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+lsyncd -nodaemon lsyncd-notes.config

+ 1 - 0
notes-app/day-2/storage/note-0.07840108117769362.txt

@@ -0,0 +1 @@
+<div>&nbsp;jfdsdsf</div><div><br></div><div><br></div><div>fdsf</div><div><br></div><div>fdsfs<br></div>

+ 1 - 0
notes-app/day-2/storage/note-0.5922552422053498.txt

@@ -0,0 +1 @@
+<div>--</div><div>yo.y<b>o</b>.</div><div>--</div><div><br></div>

+ 1 - 0
notes-app/day-3/.htpasswd

@@ -0,0 +1 @@
+demo:$apr1$DZ.vncRb$7TkCdcpB5LV4h1nQI6flh.

+ 30 - 0
notes-app/day-3/index.html

@@ -0,0 +1,30 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Notes</title>
+    <link rel="stylesheet" href="ext-2.0.2/resources/css/ext-all.css">
+
+    <script src="lunr.js"></script>
+
+    <script src="ext-2.0.2/adapter/jquery/jquery.js"></script>
+    <script src="ext-2.0.2/adapter/jquery/ext-jquery-adapter.js"></script>
+
+    <script src="ext-2.0.2/ext-all-debug.js"></script>
+    <script>
+    var viewport = new Ext.Viewport({id: 'viewport', layout: 'fit'});
+    </script>
+    <script src="notes.js"></script>
+  </head>
+  <body>
+
+<div id="notes-app">
+
+</div>
+
+<script>
+runNotesApp();
+</script>
+
+  </body>
+</html>

+ 7 - 0
notes-app/day-3/lsyncd-notes.config

@@ -0,0 +1,7 @@
+sync {
+  default.rsyncssh,
+  source = "/home/hi/p/cityapper-com-school/demos/notes-app/day-1/storage",
+  host   = "hi@cityapper.com",
+  targetdir = "/home/hi/storage/notes",
+  delete = false
+}

+ 66 - 0
notes-app/day-3/nginx.conf

@@ -0,0 +1,66 @@
+server {
+	listen 8109;
+	listen [::]:8109;
+
+	set $app_root '/home/hi/p/cityapper-com-school/demos/notes-app/day-3';
+
+	root $app_root;
+        auth_basic_user_file $app_root/.htpasswd;
+        auth_basic           "Restricted";
+
+
+
+	# Add index.php to the list if you are using PHP
+	index index.html;
+
+	#server_name hi-pi-64.local;
+
+	location / {
+		# First attempt to serve request as file, then
+		# as directory, then fall back to displaying a 404.
+		try_files $uri $uri/ =404;
+	}
+
+
+        location /storage {
+          autoindex on;
+          alias $app_root/storage;
+
+    # Preflighted requests
+    if ($request_method = OPTIONS) {
+      add_header "Access-Control-Allow-Origin" *;
+      add_header "Access-Control-Allow-Methods" "GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK, PROPFIND";
+      add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept, DNT, X-CustomHeader, Keep-Alive,User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Range, Range, Depth";
+      return 200;
+    }
+
+    if ($request_method = (GET|POST|HEAD|DELETE|PROPFIND)) {
+      add_header "Access-Control-Allow-Origin" *;
+      add_header "Access-Control-Allow-Methods" "GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK, PROPFIND";
+      add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
+    }
+
+
+    # dav allowed method
+    dav_methods     PUT DELETE MKCOL COPY MOVE;
+    # Allow current scope perform specified DAV method
+    dav_ext_methods PROPFIND OPTIONS;
+
+    
+    # In this folder, newly created folder or file is to have specified permission. If none is given, default is user:rw. If all or group permission is specified, user could be skipped
+    dav_access      user:rw group:rw all:r;
+
+    # Temporary folder
+    client_body_temp_path   /tmp;
+    
+    # MAX size of uploaded file, 0 mean unlimited
+    client_max_body_size    0;
+    
+    # Allow autocreate folder here if necessary
+    create_full_put_path    on;
+
+
+
+
+        }
+}

+ 292 - 0
notes-app/day-3/notes.js

@@ -0,0 +1,292 @@
+// Returns a function, that, as long as it continues to be invoked, will not
+// be triggered. The function will be called after it stops being called for
+// N milliseconds. If `immediate` is passed, trigger the function on the
+// leading edge, instead of the trailing.
+function debounce(func, wait, immediate) {
+        var timeout;
+        return function() {
+                var context = this, args = arguments;
+                var later = function() {
+                        timeout = null;
+                        if (!immediate) func.apply(context, args);
+                };
+                var callNow = immediate && !timeout;
+                clearTimeout(timeout);
+                timeout = setTimeout(later, wait);
+                if (callNow) func.apply(context, args);
+        };
+};
+
+
+// we can do this as a node script or in the browser.
+// as a node script we don't need to request but we
+// need local access to the files. webdavfs
+// could fill the gap, since we'd have the 
+// credentials to read notes remotely anyway.
+function indexNotes () {
+
+  // for each path, get the note content
+
+  // return makeIndex(notes);
+}
+
+function makeIndex (notes) {
+  var idx = lunr(function () {
+    this.ref('path');
+    this.field('body');
+
+    this.metadataWhitelist = ['position'];
+
+    var builder = this;
+
+    var i, note;
+    for (i = 0, note = notes[i]; i < notes.length; i++) {
+      builder.add({
+        "path": note.path,
+        "body": note.body
+      });
+    }
+
+
+  });
+
+  return idx;
+}
+
+function addDocument(builder) {
+
+};
+
+
+}
+
+function WebDavFileSystem(baseUrl) {
+  this.baseUrl = baseUrl || "/storage";
+}
+
+WebDavFileSystem.prototype.list = function list(path) {
+  var baseUrl = this.baseUrl;
+
+  var url = baseUrl + path;
+
+
+
+  return fetch(url, {method: "PROPFIND", credentials: 'include',  mode: 'cors'})
+    .then(function (r) { return r.text(); })
+    .then(function (str) { return new window.DOMParser().parseFromString(str, "text/xml"); })
+    .then(function (r) {
+      console.log(r);
+      var items = r.querySelectorAll("multistatus response");
+      items = Array.prototype.slice.call(items);
+      items = items.map(function (i) {
+        var i = {
+          path: i.querySelector("href").textContent,
+          created_at: i.querySelector("prop creationdate").textContent,
+          modified_at: i.querySelector("prop getlastmodified").textContent,
+          length_bytes: i.querySelector("prop getcontentlength").textContent,
+          display_name: i.querySelector("prop displayname").textContent,
+          status: i.querySelector("status").textContent
+        };
+
+        if (i.path) {
+          var baseUrlParts = new URL(baseUrl, document.baseURI);
+          i.path = i.path.substr(baseUrlParts.pathname.length);
+        }
+
+        return i;
+      });
+
+
+
+      return items;
+    });
+
+
+}
+
+WebDavFileSystem.prototype.upload = function upload(path, contentType, content) {
+  var url = this.baseUrl + path;
+  return fetch(url, {method: 'PUT', body: content, credentials: 'include',  mode: 'cors'});
+}
+
+WebDavFileSystem.prototype.download = function download(path) {
+  var url = this.baseUrl + path;
+  return fetch(url, {credentials: 'include',  mode: 'cors'})
+    .then(function (r) { return r.text(); })
+}
+
+
+var fs = new WebDavFileSystem();
+
+var notesStore;
+
+
+function saveNote() {
+  // post note to /upload
+  // post new .csv to /upload (logic should be on server)
+}
+
+function syncNotesFromDir() {
+  fs.list("/").then(function (files) {
+    var IDs = files.filter(function (f) {
+      return f.path.endsWith(".txt");
+    });
+
+    var data = IDs.map(function (id) {
+      return [id.path, id.created_at, id.modified_at];
+    })
+    console.log(data);
+    //notesStore.clear();
+    notesStore.loadData(data, true);
+  });
+}
+var notesStore = new Ext.data.SimpleStore({
+  fields: ["id", "created", "modified", "title", "body", "attachment_keys"],
+  id: 0,
+  data: []
+});
+notesStore.on("add", function (store, records, idx) {
+  console.log("Notes added: ");
+  console.log(records);
+  var noteNodes = records.map(noteToNode);
+  var notesFolders = Ext.ComponentMgr.get("notesFolders");
+  var notesNode = notesFolders.root;
+  notesNode.appendChild(noteNodes);
+});
+
+function noteToNode(record) {
+  var title = record.data.id;
+  var node = new Ext.tree.TreeNode({
+    text: title,
+    leaf: true,
+    noteId: record.data.id
+  });
+  return node;
+}
+
+
+
+function  runNotesApp () {
+    Ext.QuickTips.init();
+    var viewport = Ext.getCmp('viewport');
+
+    var win;
+    var selectedNoteId = null;
+    if (!win) {
+      var noteNodes = notesStore.getRange().map(noteToNode);
+      var notesNode = new Ext.tree.TreeNode({
+        text: "Notes",
+        expanded: true
+      });
+      notesNode.appendChild(noteNodes);
+
+      function addHandler() {
+        var newId = "/note-" + Math.random() + ".txt";
+        var date = new Date().toString();
+        newId = prompt("ID for new note:", newId);
+        // this should be the effect of a dispatch.
+        fs.upload(newId, "text/plain", "").then(function () {
+
+          notesStore.loadData([
+            [newId, date, date]
+          ], true);
+          var note = notesStore.getById(newId);
+          //notesNode.appendChild(noteToNode(note));
+        });
+      }
+      var foldersComp = {
+        xtype: "treepanel",
+        id: "notesFolders",
+        tbar: [{
+          text: 'Add',
+          handler: addHandler
+        }],
+        root: notesNode,
+        listeners: {
+          click: function (node, evt) {
+            var id = node.attributes.noteId;
+            var editor = Ext.ComponentMgr.get("noteEditor");
+            selectedNoteId = id; // includes deselect, eg. folder.
+            if (!id) {
+              editor.setValue("");
+              return;
+            }
+            console.log("note click: " + id);
+            var note = notesStore.getById(id);
+            //var noteHtml = note.data.body;
+            fs.download( note.data.id ).catch(function (err) {
+              console.log("error: " + err.httpStatus);
+            }).then(function (noteHtml) {
+              var editor = Ext.ComponentMgr.get("noteEditor");
+              editor.setValue(noteHtml);
+            });
+          }
+        }
+      };
+      var foldersConfig = {
+        region: "west",
+        split: true,
+        width: 160,
+        layout: "fit",
+        border: false,
+        items: [foldersComp]
+      };
+      var noteConfig = {
+        region: "center",
+        split: true,
+        border: false,
+        layout: "border",
+        items: [{
+          region: "center",
+          xtype: "htmleditor",
+          html: "Note.",
+          id: "noteEditor",
+          border: false
+        }, {
+          region: "south",
+          border: false,
+          html: "Note stats."
+        }]
+      };
+      var searchConfig = {
+        xtype: "textfield",
+        width: 150,
+        emptyText: "Search..."
+      };
+      win = new Ext.Panel({
+        tbar: [{
+          xtype: "tbspacer"
+        }, searchConfig],
+        id: "notes-win",
+        header: false,
+        title: "Notes",
+        layout: "border",
+        bodyBorder: false,
+        autoShow: true,
+        //border: false,
+        //height: 280,
+        //width: 460,
+        items: [foldersConfig, noteConfig],
+        //bbar: []
+      });
+    }
+
+    viewport.add(win);
+    viewport.doLayout();
+    //win.show();
+    //win.maximize();
+
+    syncNotesFromDir();
+    var editor = Ext.ComponentMgr.get("noteEditor");
+    editor.on("sync", debounce(function (e, html) {
+      console.log("Editor sync. Selected = " + selectedNoteId);
+      if (!selectedNoteId) {
+        return;
+      }
+      var note = notesStore.getById(selectedNoteId);
+      fs.upload(note.data.id, "text/plain", html);
+      //note.beginEdit();
+      //note.set("body", html);
+      //note.endEdit();
+    }, 750));
+}

+ 18 - 0
notes-app/day-3/scripts/setup-deb-bullseye.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+set -e;
+
+mkdir -p storage
+
+sudo chgrp www-data storage \
+                   .htpasswd \
+                   nginx.conf
+
+sudo chmod 770 storage
+sudo chmod g+s storage
+
+sudo chmod 640 .htpasswd nginx.conf
+
+sudo ln -sf $PWD/nginx.conf /etc/nginx/sites-enabled/notes-app.conf
+
+sudo nginx -t
+sudo service nginx reload

+ 3 - 0
notes-app/day-3/scripts/sync-notes

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+lsyncd -nodaemon lsyncd-notes.config

+ 1 - 0
notes-app/day-3/storage/note-0.07840108117769362.txt

@@ -0,0 +1 @@
+<div>&nbsp;jfdsdsf</div><div><br></div><div><br></div><div>fdsf</div><div><br></div><div>fdsfs<br></div>

+ 0 - 0
notes-app/day-3/storage/note-0.5922552422053498.txt