Browse Source

Fixes #38 Force server reload
Allow user to reboot the server process

isundil 10 months ago
parent
commit
16e99f81f6

+ 2 - 1
main.js

@@ -15,6 +15,7 @@ function App() {
     this.routerUtils = new RouterUtils(this);
     this.databaseHelper = require('craftlabhttpserver/src/databaseHelper').DatabaseHelper;
     this.libraryManager = require('./src/libraryManager.js').LibraryManager;
+    this.server = null;
 }
 
 App.prototype.init = async function() {
@@ -36,7 +37,7 @@ App.prototype.init = async function() {
 }
 
 App.prototype.run = async function() {
-    http.createServer(this.router).listen(CONFIG.port);
+    this.server = http.createServer(this.router).listen(CONFIG.port);
     await this.libraryManager.updateLibraries(this);
     setInterval(async () => {
         await this.libraryManager.updateLibraries(this);

+ 22 - 0
router/api.js

@@ -44,7 +44,29 @@ async function accessListToJson(app, req) {
 }
 
 module.exports = { register: app => {
+    app.router.post("/api/server/reboot", async (req, res) => {
+        app.routerUtils.onApiRequest(req, res);
+        if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList))
+            return app.routerUtils.onBadRequest(res);
+        console.log("Starting reboot process, initiaed from ", res.sessionObj);
+        app.server.close(() => {
+            require("child_process").spawn(process.argv.shift(), process.argv, {
+                cwd: process.cwd(),
+                detached : true,
+                stdio: "inherit"
+            }).unref();
+            setTimeout(() => process.exit(), 500);
+        });
+        app.routerUtils.jsonResponse(res, {});
+    });
     app.router.post("/api/database/reload", async (req, res) => {
+        app.routerUtils.onApiRequest(req, res);
+        if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList))
+            return app.routerUtils.onBadRequest(res);
+        app.libraryManager.forceReload(app);
+        app.routerUtils.jsonResponse(res, {});
+    });
+    app.router.post("/api/database/scan", async (req, res) => {
         app.routerUtils.onApiRequest(req, res);
         if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList))
             return app.routerUtils.onBadRequest(res);

+ 53 - 33
src/libraryManager.js

@@ -3,48 +3,68 @@ const Path = require('path');
 const CONFIG = require('./config.js');
 const Library = require('./library.js').Library;
 const BuilderClass = require('./autotagBuilder');
+const MediaFileModel = require('../model/mediaItem.js').MediaFileModel;
 
-function LibraryManager() {
-    this.libraries = [];
-    this.updating = false;
+class LibraryManager {
+    libraries = [];
+    updating = false;
 
-    for (let i of CONFIG.photoLibraries)
-        this.push(i);
-}
+    constructor() {
+        for (let i of CONFIG.photoLibraries)
+            this.push(i);
+    }
 
-LibraryManager.prototype.push = function(path) {
-    this.libraries.push(new Library(path));
-}
+    push(path) {
+        this.libraries.push(new Library(path));
+    }
 
-LibraryManager.prototype.updateLibraries = async function(app) {
-    if (this.updating)
-        return;
-    this.updating = true;
-    await Promise.allSettled(this.libraries.map(i => i.updateLibrary(app.databaseHelper)));
-    await BuilderClass.rebuildPathTags(app);
-    this.updating = false;
-}
+    async #doUpdateLibraries(app) {
+        await Promise.allSettled(this.libraries.map(i => i.updateLibrary(app.databaseHelper)));
+        await BuilderClass.rebuildPathTags(app);
+    }
 
-LibraryManager.prototype.isUpdating = function()
-{ return this.updating; }
+    async updateLibraries(app) {
+        if (this.updating)
+            return;
+        this.updating = true;
+        await this.#doUpdateLibraries(app);
+        this.updating = false;
+    }
 
-LibraryManager.prototype.removeMedia = async function(path) {
-    path = Path.normalize(path);
-    for (let i of this.libraries) {
-        if (await i.removeMedia(path))
-            return true;
+    async #doForceReload(app) {
+        await app.databaseHelper.rawUpdate(MediaFileModel, null, { md5Sum: '00' });
+    }
+
+    async forceReload(app) {
+        if (this.updating)
+            return;
+        this.updating = true;
+        await this.#doForceReload(app);
+        await this.#doUpdateLibraries(app);
+        this.updating = false;
+    }
+
+    isUpdating = function()
+    { return this.updating; }
+
+    async removeMedia(path) {
+        path = Path.normalize(path);
+        for (let i of this.libraries) {
+            if (await i.removeMedia(path))
+                return true;
+        }
+        return false;
     }
-    return false;
-}
 
-LibraryManager.prototype.findMedia = async function(path) {
-    path = Path.normalize(path);
-    for (let i of this.libraries) {
-        let media = await i.findMedia(path);
-        if (media)
-            return media;
+    async findMedia(path) {
+        path = Path.normalize(path);
+        for (let i of this.libraries) {
+            let media = await i.findMedia(path);
+            if (media)
+                return media;
+        }
+        return null;
     }
-    return null;
 }
 
 module.exports.LibraryManager = new LibraryManager();

+ 9 - 5
static/public/css/style.css

@@ -18,6 +18,7 @@ body.overlay-visible #fullScreenOverlay {
 #pch-navbar .dropdown-menu[data-bs-popper] {
     left: initial;
     right: 0;
+    white-space: nowrap;
 }
 
 #pch-navbar #accessListMenu .accessItem .logout {
@@ -328,7 +329,9 @@ body.media-selection #pch-navbar .navbar-nav .nav-item > a#pch-navbar-unselectAl
     color: black;
 }
 
-.login-wrapper,.share-wrapper {
+#pch-page > .modal,
+#pch-page > .login-wrapper,
+#pch-page > .share-wrapper {
     position: fixed;
     justify-content: center;
     align-items: center;
@@ -344,7 +347,7 @@ body.media-selection #pch-navbar .navbar-nav .nav-item > a#pch-navbar-unselectAl
     display: none;
 }
 
-body.login-visible .login-wrapper {
+body.login-visible #pch-page > .login-wrapper {
     display: flex;
 }
 
@@ -371,14 +374,15 @@ body.login-visible .login-wrapper {
 .share-wrapper.hidden {
     display: none;
 }
-.share-wrapper {
+#pch-page > .share-wrapper {
     display: flex;
     text-align: left;
 }
-#pch-share .modal-title {
+.modal-title {
     flex: 1;
+    text-align: left;
 }
-.share-wrapper .modal {
+#pch-page > .share-wrapper .modal {
     display: inline-flex;
     flex-direction: column;
     justify-content: center;

+ 100 - 0
static/public/js/alert.js

@@ -0,0 +1,100 @@
+
+// window.confirm(text);
+// window.confirm(title, text);
+// window.confirm(title, text, options);
+// window.confirm(text, options);
+window.confirm = function(/* ... */) {
+    var title = typeof(arguments[1]) === "string" ? arguments[0] : "Photochamber";
+    var text = typeof(arguments[1]) === "string" ? arguments[1] : arguments[0];
+    var optionsParams = arguments[2] || arguments[1] || {};
+    var options = {
+        closeButtonValue: false,
+        displayCloseButton: true,
+        canCancel: true,
+        okButtonText: "Ok",
+        cancelButtonText: "Cancel"
+    };
+    for (var i in options)
+        if (optionsParams[i])
+            options[i] = optionsParams[i];
+
+    return new Promise(ok => {
+        document.body.classList.add("overlay-visible");
+        var modalContainer = document.createElement("div");
+        modalContainer.className = "modal";
+        modalContainer.tabIndex = "-1";
+        modalContainer.role = "dialog";
+        var modal = document.createElement("div");
+        modal.className = "modal-dialog";
+        var modalContent = document.createElement("div");
+        modalContent.className = "modal-content";
+        var modalTitle = document.createElement("div");
+        modalTitle.className = "modal-header";
+        var modalTitleLabel = document.createElement("h5");
+        modalTitleLabel.textContent = title;
+        modalTitleLabel.className = "modal-title";
+        modalTitle.appendChild(modalTitleLabel);
+        if (options.displayCloseButton) {
+            var closeBt = document.createElement("button");
+            closeBt.type = "button";
+            closeBt.class = "close";
+            closeBt.addEventListener("click", e => closeHandler());
+            modalTitle.appendChild(closeBt);
+            var span = document.createElement("span");
+            span.ariaHidden = true;
+            span.innerHTML = "×";
+            closeBt.appendChild(span);
+        }
+        var modalBody = document.createElement("div");
+        modalBody.className = "modal-body";
+        modalBody.textContent = text;
+        var modalFooter = document.createElement("div");
+        modalFooter.className = "modal-footer";
+
+        var okButton = document.createElement("button");
+        okButton.type = "button";
+        okButton.className = "btn";
+        okButton.textContent = options.okButtonText;
+        okButton.addEventListener("click", () => {
+            confirmValue(true);
+        });
+        modalFooter.appendChild(okButton);
+
+        if (options.canCancel) {
+            var cancelButton = document.createElement("button");
+            cancelButton.type = "button";
+            cancelButton.className = "btn btn-danger";
+            cancelButton.textContent = options.cancelButtonText;
+            cancelButton.addEventListener("click", () => {
+                confirmValue(false);
+            });
+            modalFooter.appendChild(cancelButton);
+        }
+
+        modalContainer.style.display = "initial";
+        modalContainer.appendChild(modal);
+        modal.appendChild(modalContent);
+        modalContent.appendChild(modalTitle);
+        modalContent.appendChild(modalBody);
+        modalContent.appendChild(modalFooter);
+        document.getElementById("pch-page").appendChild(modalContainer);
+
+        let closeHandler = () => {
+            confirmValue(options.closeButtonValue);
+        };
+
+        function confirmValue(val) {
+            if (options.displayCloseButton)
+                document.removeOnClosePopinRequested(closeHandler);
+            document.getElementById("pch-page").removeChild(modalContainer);
+            document.body.classList.remove("overlay-visible");
+            ok(val);
+        };
+
+        if (options.displayCloseButton) {
+            document.onClosePopinRequested(closeHandler);
+        }
+    });
+}
+
+

+ 17 - 1
static/public/js/common.js

@@ -61,7 +61,23 @@ $(() => {
             type: "POST",
             data: "{}"
         });
-    }
+    };
+
+    window.scanServerDb = () => {
+        $.ajax({
+            url: "/api/database/scan",
+            type: "POST",
+            data: "{}"
+        });
+    };
+
+    window.rebootServer = () => {
+        $.ajax({
+            url: "/api/server/reboot",
+            type: "POST",
+            data: "{}"
+        });
+    };
 
     document.Title.pop();
     AccessManager.RebuildAccess();

+ 2 - 2
static/public/js/uiAccess.js

@@ -53,11 +53,11 @@ async function logout(accessId, linkId) {
 
 window.ReloadAccessList = function(accessList) {
     if (accessList.isAdmin) {
-        document.getElementById("pch-navbar-reload").classList.remove("hidden");
+        document.getElementById("menu-dbAdmin-container").classList.remove("hidden");
         document.getElementById("pch-navbar-share").classList.remove("hidden");
         document.getElementById("pch-navbar-autotags").classList.remove("hidden");
     } else {
-        document.getElementById("pch-navbar-reload").classList.add("hidden");
+        document.getElementById("menu-dbAdmin-container").classList.add("hidden");
         document.getElementById("pch-navbar-share").classList.add("hidden");
         document.getElementById("pch-navbar-autotags").classList.add("hidden");
     }

+ 5 - 1
static/public/js/uiCommon.js

@@ -19,10 +19,14 @@ $(() => {
     });
 
     document.getElementById("pch-navbar-unselectAll").addEventListener("click", e => { e.preventDefault(); unselectAll(); });
-    document.getElementById("pch-navbar-reload").addEventListener("click", e => { e.preventDefault(); reloadServerDb(); });
+    document.getElementById("pch-navbar-rescan").addEventListener("click", e => { e.preventDefault(); scanServerDb(); });
+    document.getElementById("pch-navbar-reboot").addEventListener("click", async e => { e.preventDefault(); (await window.confirm("The Server is about to restart", { okButtonText: "Restart" })) && rebootServer(); });
+    document.getElementById("pch-navbar-reloadMeta").addEventListener("click", async e => { e.preventDefault(); (await window.confirm("The entire library will be processed again, which may take several time", { okButtonText: "Process" })) && reloadServerDb(); });
     document.getElementById("pch-navbar-share").addEventListener("click", e => { e.preventDefault(); showShareUi(); });
     document.getElementById("pch-navbar-autotags").addEventListener("click", e => { e.preventDefault(); showAutoTagsUi(); });
     document.getElementById("fullScreenOverlay").addEventListener("click", e => { e.preventDefault(); triggerClosePopinsRequestHandlers(); });
+
     document.onClosePopinRequested = (hndl) => closePopinsRequestedHandlers.push(hndl);
+    document.removeOnClosePopinRequested = (hndl) => closePopinsRequestedHandlers.splice(closePopinsRequestedHandlers.indexOf(hndl), 1);
 });
 

+ 1 - 0
templates/_footer.js

@@ -9,6 +9,7 @@ module.exports = `
 <script src="/public/leaflet/leaflet-src.js"></script>
 <script src="/public/js/Control.Geocoder.js"></script>
 
+<script src="/public/js/alert.js"></script>
 <script src="/public/js/taskQueue.js"></script>
 <script src="/public/js/access.js"></script>
 <script src="/public/js/filters.js"></script>

+ 18 - 2
templates/_menu.js

@@ -8,8 +8,24 @@ module.exports = `
                 <li class="nav-item">
                     <a class="nav-link" href="#" role="button" id="pch-navbar-unselectAll"><i class="bi bi-check2-all">&nbsp;</i></a>
                 </li>
-                <li class="nav-item">
-                    <a class="nav-link hidden" href="#" role="button" id="pch-navbar-reload"><i class="bi bi-database-gear">&nbsp;</i></a>
+                <li class="nav-item dropdown hidden" id="menu-dbAdmin-container">
+                    <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+                        <i class="bi bi-gear">&nbsp;</i>
+                    </a>
+                    <ul class="dropdown-menu" id="menu-dbAdmin">
+                        <li id="pch-navbar-rescan"><a class="nav-link" href="#" role="button">
+                            <span class="bi bi-database-gear"></span>
+                            <span>Scan for new images</span>
+                        </a></li>
+                        <li id="pch-navbar-reloadMeta"><a class="nav-link" href="#" role="button">
+                            <span class="bi bi-database-gear"></span>
+                            <span>Reload Meta</span>
+                        </a></li>
+                        <li id="pch-navbar-reboot"><a class="nav-link" href="#" role="button">
+                            <span class="bi bi-bootstrap-reboot"></span>
+                            <span>Restart Photochamber server</span>
+                        </a></li>
+                    </ul>
                 </li>
                 <li class="nav-item">
                     <a class="nav-link hidden" href="#" role="button" id="pch-navbar-share"><i class="bi bi-share">&nbsp;</i></a>