├── src ├── data │ └── .gitkeep ├── config │ └── .gitkeep ├── utils │ ├── authCheck.js │ ├── readFile.js │ └── writeFile.js ├── routes │ ├── add_bookmark.js │ ├── update_bookmark.js │ ├── export_to_elysian.js │ ├── import_from_elysian.js │ ├── delete_bookmark.js │ ├── move_bookmark.js │ └── index.js ├── server.js └── controllers │ ├── exportToElysianController.js │ ├── importFromElysianController.js │ ├── deleteBookmarkController.js │ ├── updateBookmarkController.js │ └── addBookmarkController.js ├── .gitignore ├── .env.example ├── .dockerignore ├── assets └── Elysian_Logo.png ├── Dockerfile ├── docker-compose.yaml ├── package.json ├── LICENSE ├── .github └── workflows │ └── docker-publish.yml └── README.md /src/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.env 3 | /src/data -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API_KEY= -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /src/data/*.json 3 | Dockerfile 4 | docker-compose.yaml 5 | /assets -------------------------------------------------------------------------------- /assets/Elysian_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aadityajoshi151/Elysian/HEAD/assets/Elysian_Logo.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine 2 | WORKDIR /Elysian 3 | COPY package*.json /Elysian 4 | RUN npm install 5 | EXPOSE 6161 6 | COPY . /Elysian 7 | CMD ["npm","start"] -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | Elysian: 4 | container_name: Elysian 5 | image: aadityajoshi151/elysian:latest 6 | volumes: 7 | - ./data:/Elysian/src/data 8 | env_file: 9 | - '.env' 10 | ports: 11 | - '6161:6161' 12 | 13 | -------------------------------------------------------------------------------- /src/utils/authCheck.js: -------------------------------------------------------------------------------- 1 | const apiKey = process.env.API_KEY; 2 | 3 | function isAuthorized(req_api) { 4 | try { 5 | if (req_api === apiKey) return true //Comparing the API key in request with one from .env file 6 | else return false 7 | } 8 | catch (err) { 9 | console.error(isAuthorized.name + ': ' + err) 10 | } 11 | } 12 | 13 | module.exports = { 14 | isAuthorized 15 | }; -------------------------------------------------------------------------------- /src/routes/add_bookmark.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const addBookmarkController = require('../controllers/addBookmarkController'); 4 | 5 | //Route used when bookmark is added in browser and sent to server for addition 6 | //http://:6161/api/add_bookmark 7 | //REST Verb: POST 8 | router.post('/', express.json(), addBookmarkController.handleAddBookmark); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /src/routes/update_bookmark.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const updateBookmarkController = require('../controllers/updateBookmarkController'); 4 | 5 | //Route used when bookmark is updated in browser and sent to server for updation 6 | //http://:6161/api/update_bookmark 7 | //REST Verb: PATCH 8 | router.patch('/', express.json(), updateBookmarkController.handleUpdateBookmark); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /src/routes/export_to_elysian.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const exportToElysianController = require('../controllers/exportToElysianController'); 4 | 5 | //Route used when bookmarks are sent from browser to the server 6 | //http://:6161/api/export_to_elysian 7 | //REST Verb: POST 8 | router.post('/', express.json({limit: '10mb'}), exportToElysianController.handleExportToElysian); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /src/routes/import_from_elysian.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const importFromElysianController = require('../controllers/importFromElysianController'); 4 | 5 | //Route used when bookmarks are sent from server to the browser 6 | //http://:6161/api/import_from_elysian 7 | //REST Verb: GET 8 | router.get('/', express.json({limit: '10mb'}), importFromElysianController.handleImportFromElysian); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /src/utils/readFile.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs'); 3 | 4 | const readBookmarksFile = () => { 5 | try { 6 | const filePath = path.join(__dirname, '../data/bookmarks.json'); 7 | const bookmarks = fs.readFileSync(filePath, 'utf-8') 8 | return bookmarks; 9 | } 10 | catch (err) { 11 | console.error(readBookmarksFile.name + ': ' + err) 12 | } 13 | } 14 | 15 | module.exports = { 16 | readBookmarksFile 17 | }; -------------------------------------------------------------------------------- /src/routes/delete_bookmark.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const deleteBookmarkController = require('../controllers/deleteBookmarkController'); 4 | 5 | //Route used when bookmarkis deleted from the browser and sent to server for deletion 6 | //http://:6161/api/delete_bookmark 7 | //REST Verb: DELETE 8 | router.delete('/', express.json({limit: '10mb'}), deleteBookmarkController.handleDeleteBookmark); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /src/utils/writeFile.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs'); 3 | 4 | const createBookmarksFile = (bookmarks) => { 5 | try { 6 | const filePath = path.join(__dirname, '../data/bookmarks.json'); 7 | fs.writeFileSync(filePath, bookmarks, 'utf-8'); 8 | //console.log(createBookmarksFile.name + ': Bookmarks are saved to ' + filePath); 9 | } 10 | catch (err) { 11 | console.error(createBookmarksFile.name + ': ' + err) 12 | } 13 | } 14 | 15 | module.exports = { 16 | createBookmarksFile 17 | }; -------------------------------------------------------------------------------- /src/routes/move_bookmark.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const exportToElysianController = require('../controllers/exportToElysianController'); 4 | 5 | //Route used when bookmark is moved in browser. Moving a bookmark changes the, 6 | //indices of all other bookmarks relative to the moved bookmark hence, 7 | //export exportToElysianController is used to export all the bookmarks. 8 | //http://:6161/api/move_bookmark 9 | //REST Verb: POST 10 | router.post('/', express.json(), exportToElysianController.handleExportToElysian); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elysian", 3 | "version": "1.0.0", 4 | "description": "A self-hosted tool to backup your regularly used bookmarks from the bookmarks toolbar of your browser to your home lab.", 5 | "main": "server.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Aadityajoshi151/Elysian.git" 9 | }, 10 | "scripts": { 11 | "start": "node src/server.js" 12 | }, 13 | "keywords": [ 14 | "bookmarks", 15 | "selfhosted", 16 | "sync", 17 | "backup", 18 | "homelab" 19 | ], 20 | "author": "Aaditya Joshi", 21 | "license": "MIT", 22 | "dependencies": { 23 | "dotenv": "^16.4.5", 24 | "express": "^4.19.2" 25 | }, 26 | "devDependencies": {} 27 | } -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | //The entry point of the server 2 | 3 | const express = require('express'); 4 | const app = express(); 5 | require('dotenv').config() 6 | const routes = require('./routes'); 7 | const readFile = require('./utils/readFile'); 8 | 9 | app.use('/api', routes); 10 | 11 | app.get('/', function(req, res) { 12 | try{ 13 | bookmarks = readFile.readBookmarksFile() 14 | bookmarks = JSON.parse(bookmarks) 15 | res.json(bookmarks) 16 | } 17 | catch (error){ 18 | return res.status(500).send('Error reading bookmarks file or file not found. If this is a fresh setup of Elysian, please consider exporting your bookmarks to populate the data.'); 19 | } 20 | 21 | }); 22 | 23 | const PORT = 6161; 24 | app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); 25 | -------------------------------------------------------------------------------- /src/controllers/exportToElysianController.js: -------------------------------------------------------------------------------- 1 | const writeFile = require('../utils/writeFile'); 2 | const authCheck = require('../utils/authCheck'); 3 | 4 | const handleExportToElysian = (req, res) => { 5 | try { 6 | if (authCheck.isAuthorized(req.headers.authorization)) { //Validates API Key 7 | writeFile.createBookmarksFile(JSON.stringify(req.body)) //Writes all the bookmarks received in the file 8 | res.status(200).json('Export successful'); //Sends success response 9 | console.log(handleExportToElysian.name + ': Export successful') 10 | 11 | } 12 | else { 13 | res.status(401).json('Unauthorized'); 14 | console.error(handleExportToElysian.name + ': Unauthorized') 15 | } 16 | } 17 | catch (err) { 18 | console.error(handleExportToElysian.name+': '+err) 19 | } 20 | }; 21 | 22 | module.exports = { 23 | handleExportToElysian 24 | }; 25 | -------------------------------------------------------------------------------- /src/controllers/importFromElysianController.js: -------------------------------------------------------------------------------- 1 | const readFile = require('../utils/readFile'); 2 | const authCheck = require('../utils/authCheck'); 3 | 4 | const handleImportFromElysian = (req, res) => { 5 | try { 6 | if (authCheck.isAuthorized(req.headers.authorization)) { //Validates API Key 7 | bookmarks = readFile.readBookmarksFile() //Reads bookmarks from the file 8 | res.status(200).json(JSON.parse(bookmarks)) //Sends the bookmarks as response 9 | console.log(handleImportFromElysian.name + ': Sent bookmarks to Elysian extension') 10 | } 11 | else { 12 | res.status(401).json('Unauthorized'); 13 | console.error(handleImportFromElysian.name + ': Unauthorized') 14 | } 15 | } 16 | catch (err) { 17 | console.error(handleImportFromElysian.name + ': ' + err) 18 | } 19 | }; 20 | 21 | module.exports = { 22 | handleImportFromElysian 23 | }; 24 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | //Containers the different routes for the project 2 | 3 | const express = require('express'); 4 | const router = express.Router(); 5 | 6 | const export_to_elysian_routes = require('./export_to_elysian'); 7 | const import_from_elysian_routes = require('./import_from_elysian'); 8 | const add_bookmark_routes = require('./add_bookmark') 9 | const delete_bookmark_routes = require('./delete_bookmark') 10 | const update_bookmark_routes = require('./update_bookmark') 11 | const move_bookmark_routes = require('./move_bookmark') 12 | 13 | //Attach routes to specific paths 14 | router.use('/export_to_elysian', export_to_elysian_routes); 15 | router.use('/import_from_elysian', import_from_elysian_routes); 16 | router.use('/add_bookmark', add_bookmark_routes) 17 | router.use('/delete_bookmark', delete_bookmark_routes) 18 | router.use('/update_bookmark', update_bookmark_routes) 19 | router.use('/move_bookmark', move_bookmark_routes) 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aaditya Joshi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Elysian Docker Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'README.md' 9 | - 'assets/' 10 | - '.env.example' 11 | - '.gitgnore' 12 | - '.dockerignore' 13 | - 'docker-compose.yaml' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v2 26 | 27 | - name: Log in to Docker Hub 28 | uses: docker/login-action@v2 29 | with: 30 | username: ${{ secrets.DOCKER_USERNAME }} 31 | password: ${{ secrets.DOCKER_PASSWORD }} 32 | 33 | - name: Build and push Docker image 34 | uses: docker/build-push-action@v5 35 | with: 36 | platforms: linux/amd64,linux/arm64 37 | context: . 38 | push: true 39 | tags: ${{ secrets.DOCKER_USERNAME }}/elysian:latest 40 | 41 | - name: Image digest 42 | run: echo ${{ steps.docker_build.outputs.digest }} 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |

ELYSIAN

6 |

A self-hosted tool designed to sync your regularly used bookmarks from the bookmarks' bar of your browser to your home lab.

7 |
8 | 9 | ### How is it different from other similar services? 10 | There are numerous other selfhosted services for bookmark management like Linkding, Linkwarden etc. 11 | 12 | Those services are great. Elysian is primarily focused on backing up the bookmarks that reside in your browser's bookmarks bar and are used regularly. 13 | 14 | Elysian comes with a [browser extension](https://github.com/Aadityajoshi151/Elysian-Extension) which allows you to Import/Export bookmarks to your home lab. Once successfully connected to your home lab, and bookmarks exported, any operation you perform on your Browser's bookmarks (Create, Re-order, Update, Delete) will be automatically updated on the server side as well. 15 | 16 | ### Getting Started 17 | Follow the steps mentioned on the [wiki](https://github.com/Aadityajoshi151/Elysian/wiki/Elysian-wiki) page to get Elysian up and running. 18 | 19 | ### Future Enhancements 20 | - Browser extension for Firefox (coming soon) 21 | - A fancy server side GUI to see the hierarchical structure of the bookmarks 22 | - Button to view diff between browser and server bookmarks 23 | 24 | Feel free to open an issue if you come across any bugs or have some feature suggestions. 25 | -------------------------------------------------------------------------------- /src/controllers/deleteBookmarkController.js: -------------------------------------------------------------------------------- 1 | const readFile = require('../utils/readFile'); 2 | const writeFile = require('../utils/writeFile'); 3 | const authCheck = require('../utils/authCheck'); 4 | 5 | function deleteChildNode(bookmarks, id_to_be_deleted) { 6 | for (let i = 0; i < bookmarks.length; i++) { //For loop for searching the bookmark to be deleted 7 | const node = bookmarks[i]; 8 | if (node.id === id_to_be_deleted) { //Found bookmark to be deleted 9 | bookmarks.splice(i, 1); 10 | console.group(handleDeleteBookmark.name + ': Bookmark(s) Deleted') 11 | console.log(node) 12 | console.groupEnd() 13 | i--; 14 | continue; 15 | } 16 | //Recursion 17 | if (node.children && node.children.length > 0) { 18 | node.children = deleteChildNode(node.children, id_to_be_deleted); 19 | } 20 | } 21 | return bookmarks; 22 | } 23 | 24 | 25 | const handleDeleteBookmark = (req, res) => { 26 | try { 27 | if (authCheck.isAuthorized(req.headers.authorization)) { //Validates API Key 28 | bookmarks = readFile.readBookmarksFile() //Reads bookmarks from the file 29 | bookmarks = JSON.parse(bookmarks) //Converts them in JS object 30 | result = deleteChildNode(bookmarks, req.body.id) 31 | writeFile.createBookmarksFile(JSON.stringify(result)) //Writes the updated bookmarks (after deletion) back to the file 32 | res.status(200).json({ message: 'Bookmark Deleted' }); 33 | } 34 | else { 35 | res.status(401).json('Unauthorized'); 36 | console.error(handleDeleteBookmark.name + ': Unauthorized') 37 | } 38 | } 39 | catch (err) { 40 | console.error(handleDeleteBookmark.name + ': ' + err) 41 | } 42 | }; 43 | 44 | module.exports = { 45 | handleDeleteBookmark 46 | }; 47 | -------------------------------------------------------------------------------- /src/controllers/updateBookmarkController.js: -------------------------------------------------------------------------------- 1 | const readFile = require('../utils/readFile'); 2 | const writeFile = require('../utils/writeFile'); 3 | const authCheck = require('../utils/authCheck'); 4 | 5 | function updateChildBookmark(bookmarks, new_bookmark) { 6 | for (let i = 0; i < bookmarks.length; i++) { //For loop for searching the bookmark to be updated 7 | let node = bookmarks[i]; 8 | if (node.id === new_bookmark.id) { //Found bookmark to be updated 9 | console.group(updateChildBookmark.name) 10 | if (new_bookmark.url === undefined) { //Folder is updated (no url, only title) 11 | node.title = new_bookmark.title 12 | console.log(new_bookmark.title+' Folder updated') 13 | } 14 | else { //Bookmark is updated 15 | node.title = new_bookmark.title; 16 | node.url = new_bookmark.url; 17 | console.log(new_bookmark.title+' Bookmark updated') 18 | 19 | } 20 | console.groupEnd() 21 | } 22 | //Recursion 23 | if (node.children && node.children.length > 0) { 24 | updateChildBookmark(node.children, new_bookmark); 25 | } 26 | } 27 | } 28 | 29 | const handleUpdateBookmark = (req, res) => { 30 | try { 31 | if (authCheck.isAuthorized(req.headers.authorization)) { //Validates API Key 32 | bookmarks = readFile.readBookmarksFile() //Reads bookmarks from the file 33 | bookmarks = JSON.parse(bookmarks) //Converts them in JS object 34 | updateChildBookmark(bookmarks, req.body) 35 | writeFile.createBookmarksFile(JSON.stringify(bookmarks)) //Writes the updated bookmarks (after updation) back to the file 36 | res.status(200).json({ message: 'Bookmark Updated' }); 37 | } 38 | else { 39 | res.status(401).json('Unauthorized'); 40 | console.error(handleUpdateBookmark.name + ': Unauthorized') 41 | } 42 | } 43 | catch (err) { 44 | console.error(handleUpdateBookmark.name + ': ' + err) 45 | } 46 | }; 47 | 48 | module.exports = { 49 | handleUpdateBookmark 50 | }; -------------------------------------------------------------------------------- /src/controllers/addBookmarkController.js: -------------------------------------------------------------------------------- 1 | const readFile = require('../utils/readFile'); 2 | const writeFile = require('../utils/writeFile'); 3 | const authCheck = require('../utils/authCheck'); 4 | 5 | 6 | function addChildBookmark(bookmarks, new_bookmark) { 7 | for (let i = 0; i < bookmarks.length; i++) { 8 | let node = bookmarks[i]; 9 | if (node.id === new_bookmark.parentId) { 10 | if (!node.children) { 11 | node.children = []; 12 | } 13 | new_bookmark.index = node.children.length; 14 | node.children.push(new_bookmark); 15 | console.group(addChildBookmark.name + ': Folder/Nested Bookmark added') 16 | console.log(node.children) 17 | console.groupEnd() 18 | return true; //Nested bookmark added successfully 19 | } 20 | //Recursion 21 | if (node.children && node.children.length > 0) { 22 | let added = addChildBookmark(node.children, new_bookmark); 23 | if (added) return true; //bookmark added in the children 24 | } 25 | } 26 | return false; //Nested bookmark not added (parentId not found) 27 | } 28 | 29 | const handleAddBookmark = (req, res) => { 30 | try { 31 | if (authCheck.isAuthorized(req.headers.authorization)) { //Validates API Key 32 | bookmarks = readFile.readBookmarksFile() //Reads bookmarks from the file 33 | bookmarks = JSON.parse(bookmarks) //Converts them in JS object 34 | if (req.body.parentId === '1') { //A bookmark is added directly in bookmarks bar hence parentID=1 35 | bookmarks.push(req.body) 36 | console.group(handleAddBookmark.name + ': Bookmark added to bookmarks bar') 37 | console.log(req.body) 38 | console.groupEnd() 39 | } 40 | else { //Bookmark is added in a folder (nested bookmark) 41 | result = addChildBookmark(bookmarks, req.body) 42 | if (result == false) { 43 | console.error(handleAddBookmark.name + ': Bookmark not added. ParentID not found') 44 | } 45 | } 46 | writeFile.createBookmarksFile(JSON.stringify(bookmarks)) //Writes the updated bookmark (after addition) back to the file 47 | res.status(201).json('Bookmark Added'); 48 | } 49 | else { 50 | res.status(401).json('Unauthorized'); 51 | console.error(handleAddBookmark.name + ': Unauthorized') 52 | } 53 | } 54 | catch (err) { 55 | console.error(handleAddBookmark.name + ': ' + err) 56 | } 57 | }; 58 | 59 | module.exports = { 60 | handleAddBookmark 61 | }; 62 | --------------------------------------------------------------------------------