mirror of
https://codeberg.org/diginaut/digimarks.git
synced 2026-03-22 16:40:49 +01:00
Compare commits
33 Commits
5eb9c606f0
...
fastapi
| Author | SHA1 | Date | |
|---|---|---|---|
| 99d2011e65 | |||
| bade114b40 | |||
| 8558b518f8 | |||
| 9524fec672 | |||
| 157303aba2 | |||
| a961d90bda | |||
| 7fc7fdb171 | |||
| 1d4bc73ece | |||
| 71d5c6533d | |||
| dae6c5da18 | |||
| f68daf4ac0 | |||
| be34c6e88f | |||
| 47a0f31ec3 | |||
| 05fa94ef41 | |||
| b4aff120c8 | |||
| 82e4202482 | |||
| 9b03d51276 | |||
| fe734d6dd8 | |||
| 2936a4815a | |||
| 09c685f2aa | |||
| 0b08f0fa81 | |||
| 77dd621280 | |||
| a9f8236ee6 | |||
| ac9e010808 | |||
| 21f5f34e4f | |||
| 971ede6067 | |||
| 96a8946a9a | |||
| 14f09a2dfb | |||
| 9d813b7ea6 | |||
| 79be98abea | |||
| a7498a2fba | |||
| 8810a47faa | |||
| cae9ebf3ef |
1
.envrc.example
Normal file
1
.envrc.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
layout uv
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -77,10 +77,15 @@ celerybeat-schedule
|
|||||||
|
|
||||||
# dotenv
|
# dotenv
|
||||||
.env
|
.env
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.envrc
|
||||||
|
|
||||||
# virtualenv
|
# virtualenv
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
|
.venv
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
@@ -93,6 +98,10 @@ ENV/
|
|||||||
|
|
||||||
# vim
|
# vim
|
||||||
*.swp
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Zed editor
|
||||||
|
.zed
|
||||||
|
|
||||||
# digimarks
|
# digimarks
|
||||||
static/favicons
|
static/favicons
|
||||||
|
|||||||
130
README.md
Normal file
130
README.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# 🔖 digimarks
|
||||||
|
|
||||||
|
[](https://pypi.python.org/pypi/digimarks/)
|
||||||
|
[](https://pypi.python.org/pypi/digimarks/)
|
||||||
|
[](https://app.codacy.com/app/aquatix/digimarks?utm_source=github.com&utm_medium=referral&utm_content=aquatix/digimarks&utm_campaign=badger)
|
||||||
|
|
||||||
|
|
||||||
|
## 📚 Overview
|
||||||
|
|
||||||
|
Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags and automatic title fetching. Notes can be added, the items are cached locally in the browser, and the API is documented so everything can be accessed through that too.
|
||||||
|
|
||||||
|
[digimarks source](https://codeberg.org/diginaut/digimarks)
|
||||||
|
|
||||||
|
|
||||||
|
## 📥 Installation
|
||||||
|
|
||||||
|
There are a few ways to install digimarks to your computer or server.
|
||||||
|
|
||||||
|
### From PyPI
|
||||||
|
|
||||||
|
Assuming you already are inside a virtualenv:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using the wonderfully fast uv
|
||||||
|
uv pip install digimarks
|
||||||
|
|
||||||
|
# Alternatively, use Python pip
|
||||||
|
pip install digimarks
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### From Git
|
||||||
|
|
||||||
|
Create a new virtualenv (if you are not already in one) and install the
|
||||||
|
necessary packages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://codeberg.org/diginaut/digimarks.git
|
||||||
|
cd digimarks
|
||||||
|
# direnv will now create or activate a virtualenv
|
||||||
|
# See https://codeberg.org/diginaut/dotfiles/src/branch/master/.config/direnv/direnvrc for direnv uv config
|
||||||
|
# If you just want to run it, no need for development dependencies
|
||||||
|
uv sync --active --no-dev
|
||||||
|
# Otherwise, install everything in the active virtualenv
|
||||||
|
uv sync --active
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## ⚙️ Migrating from version 1
|
||||||
|
|
||||||
|
To be able to use the new database schema's, you will need to migrate your existing `bookmarks.db` to one under the control of the `alembic` migrations tool.
|
||||||
|
|
||||||
|
To do so, start with making a backup of this `bookmarks.db` file to a safe place.
|
||||||
|
|
||||||
|
Then, stamp the initial migration into the database, and migrate to the latest version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initiate migrations with the first one (only needs to be done once!)
|
||||||
|
alembic stamp 115bcd2e1a38
|
||||||
|
|
||||||
|
# Apply all migrations to get up-to-date
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 🛠️ Usage / example configuration
|
||||||
|
|
||||||
|
⚠️ OUT OF DATE! ⚠️
|
||||||
|
|
||||||
|
Copy `settings.py` from example_config to the parent directory and
|
||||||
|
configure to your needs (*at the least* change the value of `SYSTEMKEY`).
|
||||||
|
|
||||||
|
Do not forget to fill in the `MASHAPE_API_KEY` value, which you `can request on the RapidAPI website <https://rapidapi.com/realfavicongenerator/api/realfavicongenerator>`_.
|
||||||
|
|
||||||
|
Run digimarks as a service under nginx or apache and call the appropriate url's when wanted.
|
||||||
|
|
||||||
|
Url's are of the form `https://marks.example.com/<userkey>/<action>`
|
||||||
|
|
||||||
|
digimarks can also be run from the command line: `uvicorn digimarks:app --reload`
|
||||||
|
|
||||||
|
Be sure to export/set the `SECRETKEY` environment variable before running, it's needed for some management URI's.
|
||||||
|
|
||||||
|
Run `gunicorn -k uvicorn.workers.UvicornWorker` for production. For an example of how to set up a server `see this article <https://www.slingacademy.com/article/deploying-fastapi-on-ubuntu-with-nginx-and-lets-encrypt/>`_ with configuration for nginx, uvicorn, systemd, security and such.
|
||||||
|
|
||||||
|
The RQ background worker can be run from the command line: `rq worker --with-scheduler`
|
||||||
|
|
||||||
|
Url's are of the form `https://hook.example.com/app/<appkey>/<triggerkey>`
|
||||||
|
|
||||||
|
API documentation is auto-generated, and can be browsed at https://hook.example.com/docs
|
||||||
|
|
||||||
|
|
||||||
|
## 🧩 Bookmarklet
|
||||||
|
|
||||||
|
To easily save a link from your browser, open its bookmark manager and create a new bookmark with as url:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
javascript:location.href='http://marks.example.com/1234567890abcdef/add?url='+encodeURIComponent(location.href);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Creating a new user
|
||||||
|
|
||||||
|
After having set up the `settings.py` as under Usage, you can add a new user, by going to this path on your digimarks server:
|
||||||
|
|
||||||
|
```
|
||||||
|
/<secretkey>/adduser
|
||||||
|
```
|
||||||
|
|
||||||
|
where `secretkey` is the value set in settings.SYSTEMKEY
|
||||||
|
|
||||||
|
digimarks will then redirect to the bookmarks overview page of the new user. Please remember the user key (the hash in the url), as it will not be visible otherwise in the interface.
|
||||||
|
|
||||||
|
If you for whatever reason would lose this user key, just either look on the console (or webserver logs) where the list of available user keys is printed on digimarks startup, or open bookmarks.db with a SQLite editor.
|
||||||
|
|
||||||
|
|
||||||
|
## 🔧 Server configuration
|
||||||
|
|
||||||
|
- [systemd for digimarks API](https://codeberg.org/diginaut/digimarks/src/branch/fastapi/example_config/systemd/digimarks.service) which uses the [gunicorn config](https://codeberg.org/diginaut/digimarks/src/branch/fastapi/example_config/gunicorn_digimarks_conf.py)
|
||||||
|
- [nginx for digimarks API](https://codeberg.org/diginaut/digimarks/src/branch/fastapi/example_config/nginx_digimarks.conf)
|
||||||
|
- [more config](https://codeberg.org/diginaut/digimarks/src/branch/fastapi/example_config)
|
||||||
|
|
||||||
|
|
||||||
|
## ✨ What's new?
|
||||||
|
|
||||||
|
See the [Changelog](https://codeberg.org/diginaut/digimarks/src/branch/fastapi/CHANGELOG.md)
|
||||||
|
|
||||||
|
|
||||||
|
## 🙏 Attributions
|
||||||
|
|
||||||
|
'M' favicon by [Freepik](http://www.flaticon.com/free-icon/letter-m_2041)
|
||||||
70
README.rst
70
README.rst
@@ -1,13 +1,17 @@
|
|||||||
digimarks
|
🔖 digimarks
|
||||||
=========
|
===========
|
||||||
|
|
||||||
|PyPI version| |PyPI license| |Code health| |Codacy|
|
|PyPI version| |PyPI license| |Code health| |Codacy|
|
||||||
|
|
||||||
Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags and automatic title fetching.
|
|
||||||
|
📚 Overview
|
||||||
|
----------
|
||||||
|
|
||||||
|
Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags and automatic title fetching. Notes can be added, the items are cached locally in the browser, and the API is documented so everything can be accessed through that too.
|
||||||
|
|
||||||
|
|
||||||
Installation
|
📥 Installation
|
||||||
------------
|
--------------
|
||||||
|
|
||||||
From PyPI
|
From PyPI
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
@@ -16,6 +20,10 @@ Assuming you already are inside a virtualenv:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# Using the wonderfully fast uv
|
||||||
|
uv pip install digimarks
|
||||||
|
|
||||||
|
# Alternatively, use Python pip
|
||||||
pip install digimarks
|
pip install digimarks
|
||||||
|
|
||||||
|
|
||||||
@@ -27,17 +35,18 @@ necessary packages:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
git clone https://github.com/aquatix/digimarks.git
|
git clone https://codeberg.org/diginaut/digimarks.git
|
||||||
cd digimarks
|
cd digimarks
|
||||||
mkvirtualenv digimarks # or whatever project you are working on
|
# direnv will now create or activate a virtualenv
|
||||||
|
# See https://codeberg.org/diginaut/dotfiles/src/branch/master/.config/direnv/direnvrc for direnv uv config
|
||||||
# If you just want to run it, no need for development dependencies
|
# If you just want to run it, no need for development dependencies
|
||||||
uv sync --active --no-dev
|
uv sync --active --no-dev
|
||||||
# Otherwise, install everything in the active virtualenv
|
# Otherwise, install everything in the active virtualenv
|
||||||
uv sync --active
|
uv sync --active
|
||||||
|
|
||||||
|
|
||||||
Migrating from version 1
|
⚙️ Migrating from version 1
|
||||||
------------------------
|
--------------------------
|
||||||
|
|
||||||
To be able to use the new database schema's, you will need to migrate your existing ``bookmarks.db`` to one under the control of the ``alembic`` migrations tool.
|
To be able to use the new database schema's, you will need to migrate your existing ``bookmarks.db`` to one under the control of the ``alembic`` migrations tool.
|
||||||
|
|
||||||
@@ -47,23 +56,24 @@ Then, stamp the initial migration into the database, and migrate to the latest v
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# Initiate migrations with the first one (only needs to be done once!)
|
||||||
alembic stamp 115bcd2e1a38
|
alembic stamp 115bcd2e1a38
|
||||||
|
|
||||||
|
# Apply all migrations to get up-to-date
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
|
|
||||||
|
|
||||||
Usage / example configuration
|
🛠️ Usage / example configuration
|
||||||
-----------------------------
|
-------------------------------
|
||||||
|
|
||||||
OUT OF DATE!
|
⚠️ OUT OF DATE! ⚠️
|
||||||
|
|
||||||
Copy ``settings.py`` from example_config to the parent directory and
|
Copy ``settings.py`` from example_config to the parent directory and
|
||||||
configure to your needs (*at the least* change the value of `SYSTEMKEY`).
|
configure to your needs (*at the least* change the value of `SYSTEMKEY`).
|
||||||
|
|
||||||
Do not forget to fill in the `MASHAPE_API_KEY` value, which you ``can request on the RapidAPI website <https://rapidapi.com/realfavicongenerator/api/realfavicongenerator>`_.
|
Do not forget to fill in the ``MASHAPE_API_KEY`` value, which you `can request on the RapidAPI website <https://rapidapi.com/realfavicongenerator/api/realfavicongenerator>`_.
|
||||||
|
|
||||||
Run digimarks as a service under nginx or apache and call the appropriate
|
Run digimarks as a service under nginx or apache and call the appropriate url's when wanted.
|
||||||
url's when wanted.
|
|
||||||
|
|
||||||
Url's are of the form ``https://marks.example.com/<userkey>/<action>``
|
Url's are of the form ``https://marks.example.com/<userkey>/<action>``
|
||||||
|
|
||||||
@@ -93,7 +103,7 @@ To easily save a link from your browser, open its bookmark manager and create a
|
|||||||
Creating a new user
|
Creating a new user
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
After having set up the ```settings.py``` as under Usage, you can add a new user, by going to this path on your digimarks server:
|
After having set up the ``settings.py`` as under Usage, you can add a new user, by going to this path on your digimarks server:
|
||||||
|
|
||||||
/<secretkey>/adduser
|
/<secretkey>/adduser
|
||||||
|
|
||||||
@@ -104,27 +114,27 @@ digimarks will then redirect to the bookmarks overview page of the new user. Ple
|
|||||||
If you for whatever reason would lose this user key, just either look on the console (or webserver logs) where the list of available user keys is printed on digimarks startup, or open bookmarks.db with a SQLite editor.
|
If you for whatever reason would lose this user key, just either look on the console (or webserver logs) where the list of available user keys is printed on digimarks startup, or open bookmarks.db with a SQLite editor.
|
||||||
|
|
||||||
|
|
||||||
Server configuration
|
🔧 Server configuration
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* `systemd for digimarks API`_ which uses the `gunicorn config`_
|
* `systemd for digimarks API`_ which uses the `gunicorn config`_
|
||||||
* `nginx for digimarks API`_
|
* `nginx for digimarks API`_
|
||||||
* `more config`_
|
* `more config`_
|
||||||
|
|
||||||
|
|
||||||
What's new?
|
✨ What's new?
|
||||||
-----------
|
-------------
|
||||||
|
|
||||||
See the `Changelog`_.
|
See the `Changelog`_.
|
||||||
|
|
||||||
|
|
||||||
Attributions
|
🙏 Attributions
|
||||||
------------
|
--------------
|
||||||
|
|
||||||
'M' favicon by `Freepik`_.
|
'M' favicon by `Freepik`_.
|
||||||
|
|
||||||
|
|
||||||
.. _digimarks: https://github.com/aquatix/digimarks
|
.. _digimarks: https://codeberg.org/diginaut/digimarks
|
||||||
.. |PyPI version| image:: https://img.shields.io/pypi/v/digimarks.svg
|
.. |PyPI version| image:: https://img.shields.io/pypi/v/digimarks.svg
|
||||||
:target: https://pypi.python.org/pypi/digimarks/
|
:target: https://pypi.python.org/pypi/digimarks/
|
||||||
.. |PyPI license| image:: https://img.shields.io/github/license/aquatix/digimarks.svg
|
.. |PyPI license| image:: https://img.shields.io/github/license/aquatix/digimarks.svg
|
||||||
@@ -135,11 +145,11 @@ Attributions
|
|||||||
.. |Codacy| image:: https://api.codacy.com/project/badge/Grade/9a34319d917b43219a29e59e9ac75e3b
|
.. |Codacy| image:: https://api.codacy.com/project/badge/Grade/9a34319d917b43219a29e59e9ac75e3b
|
||||||
:alt: Codacy Badge
|
:alt: Codacy Badge
|
||||||
:target: https://app.codacy.com/app/aquatix/digimarks?utm_source=github.com&utm_medium=referral&utm_content=aquatix/digimarks&utm_campaign=badger
|
:target: https://app.codacy.com/app/aquatix/digimarks?utm_source=github.com&utm_medium=referral&utm_content=aquatix/digimarks&utm_campaign=badger
|
||||||
.. _hook settings: https://github.com/aquatix/digimarks/blob/master/example_config/examples.yaml
|
.. _hook settings: https://codeberg.org/diginaut/digimarks/blob/master/example_config/examples.yaml
|
||||||
.. _vhost for Apache2.4: https://github.com/aquatix/digimarks/blob/master/example_config/apache_vhost.conf
|
.. _vhost for Apache2.4: https://codeberg.org/diginaut/digimarks/blob/master/example_config/apache_vhost.conf
|
||||||
.. _uwsgi.ini: https://github.com/aquatix/digimarks/blob/master/example_config/uwsgi.ini
|
.. _uwsgi.ini: https://codeberg.org/diginaut/digimarks/blob/master/example_config/uwsgi.ini
|
||||||
.. _Changelog: https://github.com/aquatix/digimarks/blob/master/CHANGELOG.md
|
.. _Changelog: https://codeberg.org/diginaut/digimarks/blob/master/CHANGELOG.md
|
||||||
.. _Freepik: http://www.flaticon.com/free-icon/letter-m_2041
|
.. _Freepik: http://www.flaticon.com/free-icon/letter-m_2041
|
||||||
.. _systemd for digimarks API: https://github.com/aquatix/digimarks/blob/master/example_config/systemd/digimarks.service
|
.. _systemd for digimarks API: https://codeberg.org/diginaut/digimarks/blob/master/example_config/systemd/digimarks.service
|
||||||
.. _gunicorn config: https://github.com/aquatix/digimarks/blob/master/example_config/gunicorn_digimarks_conf.py
|
.. _gunicorn config: https://codeberg.org/diginaut/digimarks/src/branch/master/example_config/uwsgi.ini
|
||||||
.. _more config: https://github.com/aquatix/digimarks/tree/master/example_config
|
.. _more config: https://codeberg.org/diginaut/digimarks/src/branch/master/example_config
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Digimarks project."""
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<VirtualHost *:80>
|
|
||||||
ServerAdmin webmaster@example.com
|
|
||||||
ServerName marks.example.com
|
|
||||||
|
|
||||||
WSGIDaemonProcess digimarks user=youruser group=youruser threads=5 python-path=/srv/marks.example.com/digimarks/
|
|
||||||
WSGIScriptAlias / /srv/marks.example.com/digimarks/wsgi.py
|
|
||||||
|
|
||||||
<Directory /srv/marks.example.com/digimarks>
|
|
||||||
WSGIProcessGroup digimarks
|
|
||||||
WSGIApplicationGroup %{GLOBAL}
|
|
||||||
Require all granted
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
<Directory /srv/marks.example.com/digimarks>
|
|
||||||
<Files wsgi.py>
|
|
||||||
Require all granted
|
|
||||||
</Files>
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
ErrorLog /var/log/apache2/error_marks.example.com.log
|
|
||||||
|
|
||||||
# Possible values include: debug, info, notice, warn, error, crit,
|
|
||||||
# alert, emerg.
|
|
||||||
LogLevel warn
|
|
||||||
|
|
||||||
CustomLog /var/log/apache2/access_marks.example.com.log combined
|
|
||||||
ServerSignature On
|
|
||||||
|
|
||||||
</VirtualHost>
|
|
||||||
14
example_config/gunicorn_digimarks_conf.py
Normal file
14
example_config/gunicorn_digimarks_conf.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# gunicorn_conf.py
|
||||||
|
from multiprocessing import cpu_count
|
||||||
|
|
||||||
|
bind = "127.0.0.1:8890"
|
||||||
|
|
||||||
|
# Worker Options
|
||||||
|
#workers = cpu_count() + 1
|
||||||
|
workers = 1
|
||||||
|
worker_class = 'uvicorn.workers.UvicornWorker'
|
||||||
|
|
||||||
|
# Logging Options
|
||||||
|
loglevel = 'debug'
|
||||||
|
accesslog = '/var/log/digimarks/access_log'
|
||||||
|
errorlog = '/var/log/digimarks/error_log'
|
||||||
73
example_config/nginx_digimarks.conf
Normal file
73
example_config/nginx_digimarks.conf
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
server {
|
||||||
|
server_name marks.example.org;
|
||||||
|
listen [::]:443 ssl; # managed by Certbot
|
||||||
|
listen 443 ssl; # managed by Certbot
|
||||||
|
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access_marks.example.org.log;
|
||||||
|
error_log /var/log/nginx/error_marks.example.org.log warn;
|
||||||
|
|
||||||
|
# Media: images, icons, video, audio, HTC
|
||||||
|
#location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|mp3|ogg|ogv|webm|htc|woff2|woff)$ {
|
||||||
|
# expires 1M;
|
||||||
|
# access_log off;
|
||||||
|
# # max-age must be in seconds
|
||||||
|
# add_header Cache-Control "max-age=2629746, public";
|
||||||
|
#}
|
||||||
|
|
||||||
|
# CSS and Javascript
|
||||||
|
#location ~* \.(?:css|js)$ {
|
||||||
|
# expires 1M;
|
||||||
|
# access_log off;
|
||||||
|
# add_header Cache-Control "max-age=31556952, public";
|
||||||
|
#}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8890;
|
||||||
|
proxy_read_timeout 60;
|
||||||
|
proxy_connect_timeout 60;
|
||||||
|
proxy_redirect off;
|
||||||
|
|
||||||
|
# Allow the use of websockets
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /content/favicons/ {
|
||||||
|
alias /srv/www/marks.example.org/favicons/;
|
||||||
|
# This can certainly be cached, so do so for a month
|
||||||
|
expires 1M;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
alias /srv/www/marks.example.org/digimarks/src/digimarks/static/;
|
||||||
|
# This can certainly be cached, so do so for a month
|
||||||
|
#expires 1M;
|
||||||
|
#add_header Cache-Control "public";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /favicon.ico {
|
||||||
|
# Favicon for the webapp, shown in the browser
|
||||||
|
alias /srv/www/marks.example.org/digimarks/src/digimarks/static/favicon.ico;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/marks.example.org/fullchain.pem; # managed by Certbot
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/marks.example.org/privkey.pem; # managed by Certbot
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
if ($host = marks.example.org) {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
} # managed by Certbot
|
||||||
|
|
||||||
|
listen [::]:80 ;
|
||||||
|
listen 80;
|
||||||
|
server_name marks.example.org;
|
||||||
|
return 404; # managed by Certbot
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Virtualenv to use with the wsgi file (optional)
|
|
||||||
VENV = '/srv/marks.example.com/venv/bin/activate_this.py'
|
|
||||||
|
|
||||||
PORT = 8086
|
|
||||||
|
|
||||||
DEBUG = False
|
|
||||||
|
|
||||||
# Password/url key to do admin stuff with, like adding a user
|
|
||||||
# NB: change this to something else! For example, in bash:
|
|
||||||
# echo -n "yourstring" | sha1sum
|
|
||||||
SYSTEMKEY = 'S3kr1t'
|
|
||||||
|
|
||||||
# RapidAPI key for favicons
|
|
||||||
# https://rapidapi.com/realfavicongenerator/api/realfavicongenerator
|
|
||||||
MASHAPE_API_KEY = 'your_MASHAPE_key'
|
|
||||||
|
|
||||||
LOG_LOCATION = 'digimarks.log'
|
|
||||||
#LOG_LOCATION = '/var/log/digimarks/digimarks.log'
|
|
||||||
# How many logs to keep in log rotation:
|
|
||||||
LOG_BACKUP_COUNT = 10
|
|
||||||
22
example_config/systemd/digimarks.service
Normal file
22
example_config/systemd/digimarks.service
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Gunicorn Daemon for digimarks FastAPI
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=YOUR_USER
|
||||||
|
Group=YOUR_USER
|
||||||
|
WorkingDirectory=/srv/www/marks.example.org/digimarks/src
|
||||||
|
|
||||||
|
Environment="SYSTEM_KEY=RanDomSeCretKeyFoRAdmin"
|
||||||
|
Environment="FAVICONS_DIR=/srv/www/marks.example.org/favicons"
|
||||||
|
Environment="DATABASE_FILE=/srv/www/marks.example.org/bookmarks.db"
|
||||||
|
Environment="STATIC_DIR=digimarks/static"
|
||||||
|
Environment="TEMPLATE_DIR=digimarks/templates"
|
||||||
|
|
||||||
|
ExecStart=/srv/www/marks.example.org/venv/bin/gunicorn -c /srv/www/marks.example.org/gunicorn_digimarks_conf.py digimarks.main:app
|
||||||
|
|
||||||
|
StandardOutput=file:///var/log/digimarks/stdout.log
|
||||||
|
StandardError=file:///var/log/digimarks/stderr.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Example supervisord configuration
|
|
||||||
# Run with /srv/venv/bin/uwsgi --ini /srv/digimarks/uwsgi.ini:digimarks
|
|
||||||
|
|
||||||
[digimarks]
|
|
||||||
chdir = /srv/digimarks
|
|
||||||
socket = /tmp/uwsgi_digimarks.sock
|
|
||||||
module = wsgi
|
|
||||||
threads = 4
|
|
||||||
master = true
|
|
||||||
processes = 5
|
|
||||||
vacuum = true
|
|
||||||
no-orphans = true
|
|
||||||
chmod-socket = 666
|
|
||||||
logger = main file:/var/log/webapps/digimarks.log
|
|
||||||
logger = file:/var/log/webapps/digimarks_debug.log
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""Alembic environment file for SQLAlchemy."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
@@ -7,7 +9,7 @@ from sqlalchemy.engine import Connection
|
|||||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
from src.digimarks.models import Bookmark, PublicTag, User
|
from src.digimarks.models import Bookmark, PublicTag, User # noqa
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
# this is the Alembic Config object, which provides
|
||||||
# access to the values within the .ini file in use.
|
# access to the values within the .ini file in use.
|
||||||
@@ -56,6 +58,7 @@ def run_migrations_offline() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def do_run_migrations(connection: Connection) -> None:
|
def do_run_migrations(connection: Connection) -> None:
|
||||||
|
"""Run the migrations."""
|
||||||
context.configure(
|
context.configure(
|
||||||
connection=connection,
|
connection=connection,
|
||||||
target_metadata=target_metadata,
|
target_metadata=target_metadata,
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
"""Initial migration
|
"""Initial migration.
|
||||||
|
|
||||||
Revision ID: 115bcd2e1a38
|
Revision ID: 115bcd2e1a38
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2025-09-12 16:06:16.479075
|
Create Date: 2025-09-12 16:06:16.479075
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '115bcd2e1a38'
|
revision: str = '115bcd2e1a38'
|
||||||
@@ -21,7 +20,8 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema."""
|
"""Upgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('bookmark',
|
op.create_table(
|
||||||
|
'bookmark',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('userkey', sa.String(length=255), nullable=False),
|
sa.Column('userkey', sa.String(length=255), nullable=False),
|
||||||
sa.Column('title', sa.String(length=255), nullable=False),
|
sa.Column('title', sa.String(length=255), nullable=False),
|
||||||
@@ -36,23 +36,25 @@ def upgrade() -> None:
|
|||||||
sa.Column('deleted_date', sa.DateTime(), server_default=sa.text('(null)'), nullable=True),
|
sa.Column('deleted_date', sa.DateTime(), server_default=sa.text('(null)'), nullable=True),
|
||||||
sa.Column('status', sa.Integer(), server_default=sa.text('0'), nullable=True),
|
sa.Column('status', sa.Integer(), server_default=sa.text('0'), nullable=True),
|
||||||
sa.Column('note', sa.Text(), server_default=sa.text('(null)'), nullable=True),
|
sa.Column('note', sa.Text(), server_default=sa.text('(null)'), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id'),
|
||||||
)
|
)
|
||||||
op.create_table('publictag',
|
op.create_table(
|
||||||
|
'publictag',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('tagkey', sa.String(length=255), nullable=False),
|
sa.Column('tagkey', sa.String(length=255), nullable=False),
|
||||||
sa.Column('userkey', sa.String(length=255), nullable=False),
|
sa.Column('userkey', sa.String(length=255), nullable=False),
|
||||||
sa.Column('tag', sa.String(length=255), nullable=False),
|
sa.Column('tag', sa.String(length=255), nullable=False),
|
||||||
sa.Column('created_date', sa.DateTime(), server_default=sa.text('(null)'), nullable=True),
|
sa.Column('created_date', sa.DateTime(), server_default=sa.text('(null)'), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id'),
|
||||||
)
|
)
|
||||||
op.create_table('user',
|
op.create_table(
|
||||||
|
'user',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('username', sa.String(length=255), nullable=False),
|
sa.Column('username', sa.String(length=255), nullable=False),
|
||||||
sa.Column('key', sa.String(length=255), nullable=False),
|
sa.Column('key', sa.String(length=255), nullable=False),
|
||||||
sa.Column('created_date', sa.DateTime(), nullable=False),
|
sa.Column('created_date', sa.DateTime(), nullable=False),
|
||||||
sa.Column('theme', sa.String(length=20), server_default=sa.text("'green'"), nullable=True),
|
sa.Column('theme', sa.String(length=20), server_default=sa.text("'green'"), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id'),
|
||||||
)
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,14 @@
|
|||||||
Revision ID: a8d8e45f60a1
|
Revision ID: a8d8e45f60a1
|
||||||
Revises: 115bcd2e1a38
|
Revises: 115bcd2e1a38
|
||||||
Create Date: 2025-09-12 16:10:41.378716
|
Create Date: 2025-09-12 16:10:41.378716
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from datetime import UTC, datetime
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
from datetime import UTC, datetime
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlmodel
|
import sqlmodel
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = 'a8d8e45f60a1'
|
revision: str = 'a8d8e45f60a1'
|
||||||
@@ -24,72 +23,74 @@ def upgrade() -> None:
|
|||||||
"""Upgrade schema."""
|
"""Upgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
with op.batch_alter_table('bookmark', schema=None) as batch_op:
|
with op.batch_alter_table('bookmark', schema=None) as batch_op:
|
||||||
batch_op.alter_column('note',
|
batch_op.alter_column(
|
||||||
|
'note',
|
||||||
existing_type=sa.TEXT(),
|
existing_type=sa.TEXT(),
|
||||||
type_=sqlmodel.sql.sqltypes.AutoString(),
|
type_=sqlmodel.sql.sqltypes.AutoString(),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
existing_server_default=sa.text('(null)'))
|
existing_server_default=sa.text('(null)'),
|
||||||
batch_op.alter_column('starred',
|
)
|
||||||
existing_type=sa.BOOLEAN(),
|
batch_op.alter_column(
|
||||||
nullable=False,
|
'starred', existing_type=sa.BOOLEAN(), nullable=False, existing_server_default=sa.text('0')
|
||||||
existing_server_default=sa.text('0'))
|
)
|
||||||
batch_op.alter_column('modified_date',
|
batch_op.alter_column('modified_date', existing_type=sa.DATETIME(), nullable=True)
|
||||||
existing_type=sa.DATETIME(),
|
batch_op.alter_column(
|
||||||
nullable=True)
|
'deleted_date', existing_type=sa.DATETIME(), nullable=True, existing_server_default=sa.text('(null)')
|
||||||
batch_op.alter_column('deleted_date',
|
)
|
||||||
existing_type=sa.DATETIME(),
|
batch_op.alter_column(
|
||||||
nullable=True,
|
'status', existing_type=sa.INTEGER(), nullable=False, existing_server_default=sa.text('0')
|
||||||
existing_server_default=sa.text('(null)'))
|
)
|
||||||
batch_op.alter_column('status',
|
|
||||||
existing_type=sa.INTEGER(),
|
|
||||||
nullable=False,
|
|
||||||
existing_server_default=sa.text('0'))
|
|
||||||
batch_op.create_foreign_key('bookmark_user', 'user', ['userkey'], ['key'])
|
batch_op.create_foreign_key('bookmark_user', 'user', ['userkey'], ['key'])
|
||||||
with op.batch_alter_table('publictag', schema=None) as batch_op:
|
with op.batch_alter_table('publictag', schema=None) as batch_op:
|
||||||
batch_op.alter_column('created_date',
|
batch_op.alter_column(
|
||||||
|
'created_date',
|
||||||
existing_type=sa.DATETIME(),
|
existing_type=sa.DATETIME(),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
existing_server_default=sa.text(str(datetime.now(UTC))))
|
existing_server_default=sa.text(str(datetime.now(UTC))),
|
||||||
|
)
|
||||||
batch_op.create_foreign_key('publictag_user', 'user', ['userkey'], ['key'])
|
batch_op.create_foreign_key('publictag_user', 'user', ['userkey'], ['key'])
|
||||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
batch_op.alter_column('theme',
|
batch_op.alter_column(
|
||||||
existing_type=sa.VARCHAR(length=20),
|
'theme', existing_type=sa.VARCHAR(length=20), nullable=False, existing_server_default=sa.text("'green'")
|
||||||
nullable=False,
|
)
|
||||||
existing_server_default=sa.text("'green'"))
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
"""Downgrade schema."""
|
"""Downgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.alter_column('user', 'theme',
|
op.alter_column(
|
||||||
existing_type=sa.VARCHAR(length=20),
|
'user', 'theme', existing_type=sa.VARCHAR(length=20), nullable=True, existing_server_default=sa.text("'green'")
|
||||||
nullable=True,
|
)
|
||||||
existing_server_default=sa.text("'green'"))
|
|
||||||
op.drop_constraint(None, 'publictag', type_='foreignkey')
|
op.drop_constraint(None, 'publictag', type_='foreignkey')
|
||||||
op.alter_column('publictag', 'created_date',
|
op.alter_column(
|
||||||
|
'publictag',
|
||||||
|
'created_date',
|
||||||
existing_type=sa.DATETIME(),
|
existing_type=sa.DATETIME(),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
existing_server_default=sa.text('(null)'))
|
existing_server_default=sa.text('(null)'),
|
||||||
|
)
|
||||||
op.drop_constraint(None, 'bookmark', type_='foreignkey')
|
op.drop_constraint(None, 'bookmark', type_='foreignkey')
|
||||||
op.alter_column('bookmark', 'status',
|
op.alter_column(
|
||||||
existing_type=sa.INTEGER(),
|
'bookmark', 'status', existing_type=sa.INTEGER(), nullable=True, existing_server_default=sa.text('0')
|
||||||
nullable=True,
|
)
|
||||||
existing_server_default=sa.text('0'))
|
op.alter_column(
|
||||||
op.alter_column('bookmark', 'deleted_date',
|
'bookmark',
|
||||||
|
'deleted_date',
|
||||||
existing_type=sa.DATETIME(),
|
existing_type=sa.DATETIME(),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
existing_server_default=sa.text('(null)'))
|
existing_server_default=sa.text('(null)'),
|
||||||
op.alter_column('bookmark', 'modified_date',
|
)
|
||||||
existing_type=sa.DATETIME(),
|
op.alter_column('bookmark', 'modified_date', existing_type=sa.DATETIME(), nullable=True)
|
||||||
nullable=True)
|
op.alter_column(
|
||||||
op.alter_column('bookmark', 'starred',
|
'bookmark', 'starred', existing_type=sa.BOOLEAN(), nullable=True, existing_server_default=sa.text('0')
|
||||||
existing_type=sa.BOOLEAN(),
|
)
|
||||||
nullable=True,
|
op.alter_column(
|
||||||
existing_server_default=sa.text('0'))
|
'bookmark',
|
||||||
op.alter_column('bookmark', 'note',
|
'note',
|
||||||
existing_type=sqlmodel.sql.sqltypes.AutoString(),
|
existing_type=sqlmodel.sql.sqltypes.AutoString(),
|
||||||
type_=sa.TEXT(),
|
type_=sa.TEXT(),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
existing_server_default=sa.text('(null)'))
|
existing_server_default=sa.text('(null)'),
|
||||||
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
"""Renamed keys
|
"""Renamed keys.
|
||||||
|
|
||||||
Revision ID: b8cbc6957df5
|
Revision ID: b8cbc6957df5
|
||||||
Revises: a8d8e45f60a1
|
Revises: a8d8e45f60a1
|
||||||
Create Date: 2025-09-12 22:26:38.684120
|
Create Date: 2025-09-12 22:26:38.684120
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
|
||||||
import sqlmodel
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = 'b8cbc6957df5'
|
revision: str = 'b8cbc6957df5'
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ authors = [
|
|||||||
]
|
]
|
||||||
description = 'Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags, automatic title fetching and REST API calls.'
|
description = 'Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags, automatic title fetching and REST API calls.'
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.11"
|
||||||
keywords = ["bookmarks", "api"]
|
keywords = ["bookmarks", "api"]
|
||||||
license = { text = "Apache" }
|
license = { text = "Apache" }
|
||||||
classifiers = [
|
classifiers = [
|
||||||
@@ -30,6 +30,11 @@ dependencies = [
|
|||||||
"feedgen",
|
"feedgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
server = [
|
||||||
|
"uvicorn",
|
||||||
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
{ include-group = "lint" },
|
{ include-group = "lint" },
|
||||||
@@ -42,7 +47,7 @@ test = [
|
|||||||
]
|
]
|
||||||
lint = [
|
lint = [
|
||||||
"ruff>=0.1.0",
|
"ruff>=0.1.0",
|
||||||
"mypy>=1.0.0",
|
"pyrefly",
|
||||||
]
|
]
|
||||||
# Publishing on PyPI
|
# Publishing on PyPI
|
||||||
pub = [
|
pub = [
|
||||||
@@ -58,8 +63,8 @@ server = [
|
|||||||
my-script = "digimarks:app"
|
my-script = "digimarks:app"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
"Homepage" = "https://github.com/aquatix/digimarks"
|
"Homepage" = "https://codeberg.org/diginaut/digimarks"
|
||||||
"Bug Tracker" = "https://github.com/aquatix/digimarks/issues"
|
"Bug Tracker" = "https://codeberg.org/diginaut/digimarks/issues"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
# Linting and fixing, including isort
|
# Linting and fixing, including isort
|
||||||
ruff
|
ruff
|
||||||
|
# Typing
|
||||||
|
pyrefly
|
||||||
|
|
||||||
# Test suite
|
# Test suite
|
||||||
pytest
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
|
||||||
# Publishing on PyPI
|
# Publishing on PyPI
|
||||||
build
|
build
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Core application
|
# Core application
|
||||||
fastapi[all]
|
fastapi[all]
|
||||||
sqlmodel
|
sqlmodel
|
||||||
sqlalchemy
|
sqlalchemy[asyncio]
|
||||||
pydantic
|
pydantic
|
||||||
pydantic_settings
|
pydantic_settings
|
||||||
alembic
|
alembic
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""digimarks main module."""
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""Top-level package for Digimarks."""
|
||||||
|
|
||||||
|
__author__ = """Michiel Scholten"""
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import bs4
|
|||||||
import httpx
|
import httpx
|
||||||
from extract_favicon import from_html
|
from extract_favicon import from_html
|
||||||
from fastapi import Query, Request
|
from fastapi import Query, Request
|
||||||
from pydantic import AnyUrl
|
from fastapi.exceptions import HTTPException
|
||||||
|
from pydantic import AnyUrl, ValidationError
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
|
|
||||||
from digimarks import tags_service, utils
|
from digimarks import tags_service, utils
|
||||||
@@ -34,8 +35,11 @@ async def set_information_from_source(bookmark: Bookmark, request: Request) -> B
|
|||||||
"""Request the title by requesting the source url."""
|
"""Request the title by requesting the source url."""
|
||||||
logger.info('Extracting information from url %s', bookmark.url)
|
logger.info('Extracting information from url %s', bookmark.url)
|
||||||
try:
|
try:
|
||||||
result = await request.app.requests_client.get(bookmark.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
|
result = await request.app.state.requests_client.get(
|
||||||
|
str(bookmark.url), headers={'User-Agent': DIGIMARKS_USER_AGENT}
|
||||||
|
)
|
||||||
bookmark.http_status = result.status_code
|
bookmark.http_status = result.status_code
|
||||||
|
logger.info('HTTP status code %s for %s', bookmark.http_status, bookmark.url)
|
||||||
except httpx.HTTPError as err:
|
except httpx.HTTPError as err:
|
||||||
# For example, "MissingSchema: Invalid URL 'abc': No schema supplied. Perhaps you meant http://abc?"
|
# For example, "MissingSchema: Invalid URL 'abc': No schema supplied. Perhaps you meant http://abc?"
|
||||||
logger.error('Exception when trying to retrieve title for %s. Error: %s', bookmark.url, str(err))
|
logger.error('Exception when trying to retrieve title for %s. Error: %s', bookmark.url, str(err))
|
||||||
@@ -43,11 +47,12 @@ async def set_information_from_source(bookmark: Bookmark, request: Request) -> B
|
|||||||
bookmark.title = ''
|
bookmark.title = ''
|
||||||
return bookmark
|
return bookmark
|
||||||
if bookmark.http_status == 200 or bookmark.http_status == 202:
|
if bookmark.http_status == 200 or bookmark.http_status == 202:
|
||||||
html = bs4.BeautifulSoup(result.text, 'html.parser')
|
html_content = bs4.BeautifulSoup(result.text, 'html.parser')
|
||||||
try:
|
try:
|
||||||
bookmark.title = html.title.text.strip()
|
bookmark.title = html_content.title.text.strip()
|
||||||
except AttributeError:
|
except AttributeError as exc:
|
||||||
bookmark.title = ''
|
logger.error('Error while trying to extract title from URL %s: %s', str(bookmark.url), str(exc))
|
||||||
|
raise HTTPException(status_code=400, detail='Error while trying to extract title')
|
||||||
|
|
||||||
url_parts = urlparse(str(bookmark.url))
|
url_parts = urlparse(str(bookmark.url))
|
||||||
root_url = url_parts.scheme + '://' + url_parts.netloc
|
root_url = url_parts.scheme + '://' + url_parts.netloc
|
||||||
@@ -72,11 +77,15 @@ def strip_url_params(url: str) -> str:
|
|||||||
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, '', parsed.fragment))
|
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, '', parsed.fragment))
|
||||||
|
|
||||||
|
|
||||||
def update_bookmark_with_info(bookmark: Bookmark, request: Request, strip_params: bool = False):
|
async def update_bookmark_with_info(bookmark: Bookmark, request: Request, strip_params: bool = False):
|
||||||
"""Automatically update title, favicon, etc."""
|
"""Automatically update title, favicon, etc."""
|
||||||
|
if isinstance(bookmark.url, str):
|
||||||
|
# If type of the url is a 'simple' string, ensure it to be an AnyUrl
|
||||||
|
bookmark.url = AnyUrl(bookmark.url)
|
||||||
|
|
||||||
if not bookmark.title:
|
if not bookmark.title:
|
||||||
# Title was empty, automatically fetch it from the url, will also update the status code
|
# Title was empty, automatically fetch it from the url, will also update the status code
|
||||||
set_information_from_source(bookmark, request)
|
await set_information_from_source(bookmark, request)
|
||||||
|
|
||||||
if strip_params:
|
if strip_params:
|
||||||
# Strip URL parameters, e.g., tracking params
|
# Strip URL parameters, e.g., tracking params
|
||||||
@@ -92,7 +101,10 @@ async def list_bookmarks_for_user(
|
|||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
limit: Annotated[int, Query(le=10000)] = 100,
|
limit: Annotated[int, Query(le=10000)] = 100,
|
||||||
) -> Sequence[Bookmark]:
|
) -> Sequence[Bookmark]:
|
||||||
"""List all bookmarks in the database. By default, 100 items are returned."""
|
"""List all bookmarks in the database. By default, 100 items are returned.
|
||||||
|
|
||||||
|
There is a limit of 10000 items.
|
||||||
|
"""
|
||||||
result = await session.exec(
|
result = await session.exec(
|
||||||
select(Bookmark)
|
select(Bookmark)
|
||||||
.where(Bookmark.user_key == user_key, Bookmark.status != Visibility.DELETED)
|
.where(Bookmark.user_key == user_key, Bookmark.status != Visibility.DELETED)
|
||||||
@@ -121,12 +133,17 @@ async def autocomplete_bookmark(
|
|||||||
user_key: str,
|
user_key: str,
|
||||||
bookmark: Bookmark,
|
bookmark: Bookmark,
|
||||||
strip_params: bool = False,
|
strip_params: bool = False,
|
||||||
):
|
) -> Bookmark:
|
||||||
"""Autofill some fields for this (new) bookmark for user `user_key`."""
|
"""Autofill some fields for this (new) bookmark for user `user_key`."""
|
||||||
bookmark.user_key = user_key
|
bookmark.user_key = user_key
|
||||||
|
|
||||||
# Auto-fill title, fix tags etc.
|
# Auto-fill title, fix tags etc.
|
||||||
update_bookmark_with_info(bookmark, request, strip_params)
|
try:
|
||||||
|
await update_bookmark_with_info(bookmark, request, strip_params)
|
||||||
|
except ValidationError as exc:
|
||||||
|
logger.error('ValidationError while autocompleting bookmark with URL %s', bookmark.url)
|
||||||
|
logger.error('Error was: %s', str(exc))
|
||||||
|
raise HTTPException(status_code=400, detail='Error while autocompleting, likely the URL contained an error')
|
||||||
|
|
||||||
url_hash = utils.generate_hash(str(bookmark.url))
|
url_hash = utils.generate_hash(str(bookmark.url))
|
||||||
result = await session.exec(
|
result = await session.exec(
|
||||||
@@ -149,12 +166,12 @@ async def add_bookmark(
|
|||||||
user_key: str,
|
user_key: str,
|
||||||
bookmark: Bookmark,
|
bookmark: Bookmark,
|
||||||
strip_params: bool = False,
|
strip_params: bool = False,
|
||||||
):
|
) -> Bookmark:
|
||||||
"""Add new bookmark for user `user_key`."""
|
"""Add new bookmark for user `user_key`."""
|
||||||
bookmark.user_key = user_key
|
bookmark.user_key = user_key
|
||||||
|
|
||||||
# Auto-fill title, fix tags etc.
|
# Auto-fill title, fix tags etc.
|
||||||
update_bookmark_with_info(bookmark, request, strip_params)
|
await update_bookmark_with_info(bookmark, request, strip_params)
|
||||||
bookmark.url_hash = utils.generate_hash(str(bookmark.url))
|
bookmark.url_hash = utils.generate_hash(str(bookmark.url))
|
||||||
logger.info('Adding bookmark %s for user %s', bookmark.url_hash, user_key)
|
logger.info('Adding bookmark %s for user %s', bookmark.url_hash, user_key)
|
||||||
|
|
||||||
@@ -171,7 +188,7 @@ async def update_bookmark(
|
|||||||
bookmark: Bookmark,
|
bookmark: Bookmark,
|
||||||
url_hash: str,
|
url_hash: str,
|
||||||
strip_params: bool = False,
|
strip_params: bool = False,
|
||||||
):
|
) -> Bookmark:
|
||||||
"""Update existing bookmark `bookmark_key` for user `user_key`."""
|
"""Update existing bookmark `bookmark_key` for user `user_key`."""
|
||||||
result = await session.exec(
|
result = await session.exec(
|
||||||
select(Bookmark).where(
|
select(Bookmark).where(
|
||||||
@@ -190,7 +207,7 @@ async def update_bookmark(
|
|||||||
bookmark_db.sqlmodel_update(bookmark_data)
|
bookmark_db.sqlmodel_update(bookmark_data)
|
||||||
|
|
||||||
# Autofill title, fix tags, etc. where (still) needed
|
# Autofill title, fix tags, etc. where (still) needed
|
||||||
update_bookmark_with_info(bookmark, request, strip_params)
|
await update_bookmark_with_info(bookmark, request, strip_params)
|
||||||
|
|
||||||
session.add(bookmark_db)
|
session.add(bookmark_db)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
@@ -202,7 +219,7 @@ async def delete_bookmark(
|
|||||||
session,
|
session,
|
||||||
user_key: str,
|
user_key: str,
|
||||||
url_hash: str,
|
url_hash: str,
|
||||||
):
|
) -> None:
|
||||||
"""(Soft)Delete bookmark `bookmark_key` for user `user_key`."""
|
"""(Soft)Delete bookmark `bookmark_key` for user `user_key`."""
|
||||||
result = await session.get(Bookmark, {'url_hash': url_hash, 'user_key': user_key})
|
result = await session.get(Bookmark, {'url_hash': url_hash, 'user_key': user_key})
|
||||||
bookmark = result
|
bookmark = result
|
||||||
|
|||||||
6
src/digimarks/extract.py
Normal file
6
src/digimarks/extract.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from pydantic import AnyUrl
|
||||||
|
|
||||||
|
|
||||||
|
def extract_contents(title: str, url: AnyUrl, note: str):
|
||||||
|
"""Extract contents from a URL."""
|
||||||
|
return
|
||||||
@@ -4,7 +4,7 @@ import logging
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Annotated
|
from typing import Annotated, AsyncGenerator, cast
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import Depends, FastAPI, HTTPException, Query, Request
|
from fastapi import Depends, FastAPI, HTTPException, Query, Request
|
||||||
@@ -34,8 +34,10 @@ class Settings(BaseSettings):
|
|||||||
favicons_dir: DirectoryPath
|
favicons_dir: DirectoryPath
|
||||||
|
|
||||||
# inside the codebase
|
# inside the codebase
|
||||||
static_dir: DirectoryPath = 'digimarks/static'
|
# static_dir: DirectoryPath = Path('digimarks/static')
|
||||||
template_dir: DirectoryPath = 'digimarks/templates'
|
# template_dir: DirectoryPath = Path('digimarks/templates')
|
||||||
|
static_dir: DirectoryPath = DirectoryPath('digimarks/static')
|
||||||
|
template_dir: DirectoryPath = DirectoryPath('digimarks/templates')
|
||||||
|
|
||||||
media_url: str = '/static/'
|
media_url: str = '/static/'
|
||||||
|
|
||||||
@@ -52,22 +54,32 @@ engine = create_async_engine(f'sqlite+aiosqlite:///{settings.database_file}', co
|
|||||||
|
|
||||||
async def get_session() -> AsyncSession:
|
async def get_session() -> AsyncSession:
|
||||||
"""SQLAlchemy session factory."""
|
"""SQLAlchemy session factory."""
|
||||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
yield session
|
yield session
|
||||||
|
|
||||||
|
|
||||||
|
# Shorter way of getting the DB session in an endpoint
|
||||||
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(the_app: FastAPI):
|
async def lifespan(the_app: FastAPI) -> AsyncGenerator[None, None]:
|
||||||
"""Upon start, initialise an AsyncClient and assign it to an attribute named requests_client on the app object."""
|
"""Upon start, initialise an AsyncClient and assign it to an attribute named requests_client on the app object."""
|
||||||
the_app.requests_client = httpx.AsyncClient()
|
async with httpx.AsyncClient() as requests_client:
|
||||||
|
the_app.state.requests_client = requests_client
|
||||||
yield
|
yield
|
||||||
await the_app.requests_client.aclose()
|
await the_app.state.requests_client.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_requests_client(request: Request) -> httpx.AsyncClient:
|
||||||
|
"""Get the httpx client from the application object."""
|
||||||
|
return cast(httpx.AsyncClient, request.app.state.requests_client)
|
||||||
|
|
||||||
|
|
||||||
|
# Shorter way of getting the httpx client in an endpoint
|
||||||
|
RequestsDep = Annotated[AsyncSession, Depends(get_requests_client)]
|
||||||
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
app = FastAPI(lifespan=lifespan)
|
||||||
app.mount('/static', StaticFiles(directory=settings.static_dir), name='static')
|
app.mount('/static', StaticFiles(directory=settings.static_dir), name='static')
|
||||||
app.mount('/content/favicons', StaticFiles(directory=settings.favicons_dir), name='favicons')
|
app.mount('/content/favicons', StaticFiles(directory=settings.favicons_dir), name='favicons')
|
||||||
@@ -125,7 +137,7 @@ def index(request: Request):
|
|||||||
|
|
||||||
|
|
||||||
@app.get('/api/v1/admin/{system_key}/users/{user_id}', response_model=User)
|
@app.get('/api/v1/admin/{system_key}/users/{user_id}', response_model=User)
|
||||||
async def get_user(session: SessionDep, system_key: str, user_id: int) -> type[User]:
|
async def get_user(session: SessionDep, system_key: str, user_id: int) -> User:
|
||||||
"""Show user information."""
|
"""Show user information."""
|
||||||
logger.info('User %d requested', user_id)
|
logger.info('User %d requested', user_id)
|
||||||
if system_key != settings.system_key:
|
if system_key != settings.system_key:
|
||||||
@@ -200,7 +212,7 @@ async def autocomplete_bookmark(
|
|||||||
user_key: str,
|
user_key: str,
|
||||||
bookmark: Bookmark,
|
bookmark: Bookmark,
|
||||||
strip_params: bool = False,
|
strip_params: bool = False,
|
||||||
):
|
) -> Bookmark:
|
||||||
"""Autofill some fields for this (new) bookmark for user `user_key`."""
|
"""Autofill some fields for this (new) bookmark for user `user_key`."""
|
||||||
logger.info('Autocompleting bookmark %s for user %s', bookmark.url_hash, user_key)
|
logger.info('Autocompleting bookmark %s for user %s', bookmark.url_hash, user_key)
|
||||||
return await bookmarks_service.autocomplete_bookmark(session, request, user_key, bookmark, strip_params)
|
return await bookmarks_service.autocomplete_bookmark(session, request, user_key, bookmark, strip_params)
|
||||||
@@ -246,7 +258,7 @@ async def delete_bookmark(
|
|||||||
"""(Soft)Delete bookmark `bookmark_key` for user `user_key`."""
|
"""(Soft)Delete bookmark `bookmark_key` for user `user_key`."""
|
||||||
logger.info('Deleting bookmark %s for user %s', url_hash, user_key)
|
logger.info('Deleting bookmark %s for user %s', url_hash, user_key)
|
||||||
try:
|
try:
|
||||||
result = await bookmarks_service.delete_bookmark(session, user_key, url_hash)
|
_ = await bookmarks_service.delete_bookmark(session, user_key, url_hash)
|
||||||
return {'ok': True}
|
return {'ok': True}
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('Failed to delete bookmark %s', url_hash)
|
logger.exception('Failed to delete bookmark %s', url_hash)
|
||||||
@@ -273,12 +285,24 @@ async def bookmarks_changed_since(
|
|||||||
)
|
)
|
||||||
latest_created_bookmark = result.first()
|
latest_created_bookmark = result.first()
|
||||||
|
|
||||||
latest_modification = max(latest_modified_bookmark.modified_date, latest_created_bookmark.created_date)
|
# There needs to be at least one bookmark of course
|
||||||
|
if latest_created_bookmark:
|
||||||
|
latest_created_datetime = latest_created_bookmark.created_date
|
||||||
|
else:
|
||||||
|
latest_created_datetime = datetime.min
|
||||||
|
|
||||||
|
# We only have a modified datetime when at least one has been edited
|
||||||
|
if latest_modified_bookmark:
|
||||||
|
latest_modified_datetime = latest_modified_bookmark.modified_date
|
||||||
|
else:
|
||||||
|
latest_modified_datetime = datetime.min
|
||||||
|
|
||||||
|
latest_modification = max(latest_modified_datetime, latest_created_datetime)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'current_time': datetime.now(UTC),
|
'current_time': datetime.now(UTC),
|
||||||
'latest_change': latest_modified_bookmark.modified_date,
|
'latest_change': latest_modified_datetime,
|
||||||
'latest_created': latest_created_bookmark.created_date,
|
'latest_created': latest_created_datetime,
|
||||||
'latest_modification': latest_modification,
|
'latest_modification': latest_modification,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +332,7 @@ async def page_user_landing(
|
|||||||
session: SessionDep,
|
session: SessionDep,
|
||||||
request: Request,
|
request: Request,
|
||||||
user_key: str,
|
user_key: str,
|
||||||
):
|
) -> HTMLResponse:
|
||||||
"""HTML page with the main view for the user."""
|
"""HTML page with the main view for the user."""
|
||||||
result = await session.exec(select(User).where(User.key == user_key))
|
result = await session.exec(select(User).where(User.key == user_key))
|
||||||
user = result.first()
|
user = result.first()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
/*width: 80px;*/
|
/*width: 80px;*/
|
||||||
width: 66;
|
width: 66px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail img {
|
.thumbnail img {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* v0.0.2
|
* v0.0.2
|
||||||
*
|
*
|
||||||
* Created by: Michiel Scholten
|
* Created by: Michiel Scholten
|
||||||
* Source: https://github.com/aquatix/digui
|
* Source: https://codeberg.org/diginaut/digui
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Colours and themes */
|
/** Colours and themes */
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
--border-color: #d5d9d9;
|
--border-color: #d5d9d9;
|
||||||
--border-width: 1px;
|
--border-width: 1px;
|
||||||
--border-radius: 8px;
|
--border-radius: 8px;
|
||||||
|
--chip-border-radius: 2rem;
|
||||||
--shadow-color: rgba(213, 217, 217, .5);
|
--shadow-color: rgba(213, 217, 217, .5);
|
||||||
--global-theme-toggle-content: ' 🌞';
|
--global-theme-toggle-content: ' 🌞';
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ html[data-theme='nebula-dark'] {
|
|||||||
--border-color: #333;
|
--border-color: #333;
|
||||||
--border-width: 1px;
|
--border-width: 1px;
|
||||||
--border-radius: 8px;
|
--border-radius: 8px;
|
||||||
|
--chip-border-radius: 2rem;
|
||||||
--shadow-color: rgba(3, 3, 3, .5);
|
--shadow-color: rgba(3, 3, 3, .5);
|
||||||
--global-theme-toggle-content: ' 🌝';
|
--global-theme-toggle-content: ' 🌝';
|
||||||
}
|
}
|
||||||
@@ -99,6 +101,7 @@ html[data-theme='bbs'] {
|
|||||||
--border-color: #333;
|
--border-color: #333;
|
||||||
--border-width: 2px;
|
--border-width: 2px;
|
||||||
--border-radius: 0;
|
--border-radius: 0;
|
||||||
|
--chip-border-radius: 0;
|
||||||
--global-theme-toggle-content: ' 🖥️';
|
--global-theme-toggle-content: ' 🖥️';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +127,7 @@ html[data-theme='silo'] {
|
|||||||
/*--border-color: #003eaa;*/
|
/*--border-color: #003eaa;*/
|
||||||
--border-width: 2px;
|
--border-width: 2px;
|
||||||
--border-radius: 0;
|
--border-radius: 0;
|
||||||
|
--chip-border-radius: 0;
|
||||||
--global-theme-toggle-content: ' ⌨️';
|
--global-theme-toggle-content: ' ⌨️';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +444,7 @@ th, td {
|
|||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--chip-border-radius);
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
/*color: var(--text-color);*/
|
/*color: var(--text-color);*/
|
||||||
@@ -449,7 +453,7 @@ th, td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chip .button {
|
.chip .button {
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--chip-border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status */
|
/* Status */
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
/* Bookmark that is being edited, used to fill the form, etc. */
|
/* Bookmark that is being edited, used to fill the form, etc. */
|
||||||
bookmarkToEdit: Alpine.$persist({}).as('bookmarkToEdit'),
|
bookmarkToEdit: Alpine.$persist({}).as('bookmarkToEdit'),
|
||||||
bookmarkToEditError: null,
|
bookmarkToEditError: null,
|
||||||
|
bookmarkToEditVisible: false,
|
||||||
|
|
||||||
/* Loading indicator */
|
/* Loading indicator */
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -104,7 +105,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.cache[this.userKey]['tags'] = await tagsResponse.json();
|
this.cache[this.userKey]['tags'] = await tagsResponse.json();
|
||||||
|
|
||||||
/* Filter bookmarks by (blacklisted) tags */
|
/* Filter bookmarks by (blacklisted) tags */
|
||||||
await this.filterBookmarksByTags();
|
this.filterBookmarksByTags();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -147,6 +148,10 @@ document.addEventListener('alpine:init', () => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
get filteredTags() {
|
get filteredTags() {
|
||||||
|
if (this.cache[this.userKey].tags === undefined) {
|
||||||
|
console.log('Tags not yet cached');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
/* Search in the list of all tags */
|
/* Search in the list of all tags */
|
||||||
return this.cache[this.userKey].tags.filter(
|
return this.cache[this.userKey].tags.filter(
|
||||||
i => i.match(new RegExp(this.search, "i"))
|
i => i.match(new RegExp(this.search, "i"))
|
||||||
@@ -209,26 +214,76 @@ document.addEventListener('alpine:init', () => {
|
|||||||
|
|
||||||
resetEditBookmark() {
|
resetEditBookmark() {
|
||||||
this.bookmarkToEdit = {
|
this.bookmarkToEdit = {
|
||||||
|
'url_hash': '',
|
||||||
'url': '',
|
'url': '',
|
||||||
'title': '',
|
'title': '',
|
||||||
'note': '',
|
'note': '',
|
||||||
'tags': ''
|
'tags': '',
|
||||||
|
'http_status': 0,
|
||||||
|
'strip_params': false
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
async startAddingBookmark() {
|
async startAddingBookmark() {
|
||||||
/* Open 'add bookmark' page */
|
/* Open 'add bookmark' page */
|
||||||
console.log('Start adding bookmark');
|
console.log('Open "add bookmark" modal');
|
||||||
this.resetEditBookmark();
|
this.resetEditBookmark();
|
||||||
// this.show_bookmark_details = true;
|
// this.show_bookmark_details = true;
|
||||||
const editFormDialog = document.getElementById("editFormDialog");
|
const editFormDialog = document.getElementById("editFormDialog");
|
||||||
|
this.bookmarkToEditVisible = true;
|
||||||
editFormDialog.showModal();
|
editFormDialog.showModal();
|
||||||
},
|
},
|
||||||
async bookmarkURLChanged() {
|
async bookmarkURLChanged() {
|
||||||
console.log('Bookmark URL changed');
|
console.log('Bookmark URL changed');
|
||||||
// let response = await fetch('/api/v1/' + this.userKey + '/autocomplete_bookmark/');
|
// let response = await fetch('/api/v1/' + this.userKey + '/autocomplete_bookmark/');
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/' + this.userKey + '/autocomplete_bookmark/', {
|
const response = await fetch('/api/v1/' + this.userKey + '/autocomplete_bookmark/?strip_params=' + this.bookmarkToEdit.strip_params, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
// Bookmark form data
|
||||||
|
url_hash: this.bookmarkToEdit.url_hash,
|
||||||
|
url: this.bookmarkToEdit.url,
|
||||||
|
title: this.bookmarkToEdit.title,
|
||||||
|
note: this.bookmarkToEdit.note,
|
||||||
|
tags: this.bookmarkToEdit.tags
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
// TODO: update form fields if needed (auto-fetched title for example
|
||||||
|
console.log('Got response');
|
||||||
|
console.log(response);
|
||||||
|
console.log(data);
|
||||||
|
if (response.ok) {
|
||||||
|
this.bookmarkToEdit.url_hash = data.url_hash;
|
||||||
|
this.bookmarkToEdit.url = data.url;
|
||||||
|
this.bookmarkToEdit.title = data.title;
|
||||||
|
this.bookmarkToEdit.note = data.note;
|
||||||
|
this.bookmarkToEdit.tags = data.tags;
|
||||||
|
this.bookmarkToEdit.http_status = data.http_status;
|
||||||
|
} else {
|
||||||
|
console.log('Error occurred');
|
||||||
|
this.bookmarkToEditError = data.detail;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// enter logic for when there is an error (ex. error toast)
|
||||||
|
console.log('error occurred');
|
||||||
|
console.log(error);
|
||||||
|
this.bookmarkToEditError = error.detail;
|
||||||
|
console.log('yesssh?');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveBookmark() {
|
||||||
|
console.log('Saving bookmark');
|
||||||
|
// this.bookmarkToEditVisible = false;
|
||||||
|
// this.show_bookmark_details = false;
|
||||||
|
},
|
||||||
|
async addBookmark() {
|
||||||
|
/* Post new bookmark to the backend */
|
||||||
|
console.log('Adding bookmark');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/v1/' + this.userKey + '/add_bookmark/?strip_params=' + this.bookmarkToEdit.strip_params, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -244,20 +299,12 @@ document.addEventListener('alpine:init', () => {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
// TODO: update form fields if needed (auto-fetched title for example
|
// TODO: update form fields if needed (auto-fetched title for example
|
||||||
console.log(data);
|
console.log(data);
|
||||||
this.bookmarkToEditError = 'lolwut';
|
// this.bookmarkToEditError = 'lolwut';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// enter your logic for when there is an error (ex. error toast)
|
// enter your logic for when there is an error (ex. error toast)
|
||||||
|
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
async saveBookmark() {
|
|
||||||
console.log('Saving bookmark');
|
|
||||||
// this.show_bookmark_details = false;
|
|
||||||
},
|
|
||||||
async addBookmark() {
|
|
||||||
/* Post new bookmark to the backend */
|
|
||||||
//
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
|
||||||
<link rel="manifest" href="/static/images/site.webmanifest">
|
<link rel="manifest" href="/static/images/site.webmanifest">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
{#-
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-clipboard@2.x.x/dist/alpine-clipboard.js"
|
<script src="https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-clipboard@2.x.x/dist/alpine-clipboard.js"
|
||||||
defer></script>
|
defer></script>
|
||||||
|
#}
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><h1>digimarks</h1></li>
|
<li><h1>digimarks</h1></li>
|
||||||
<li>
|
<li>
|
||||||
<a class="button" href="https://github.com/aquatix/digimarks">digimarks project page</a>
|
<a class="button" href="https://codeberg.org/diginaut/digimarks">digimarks project page</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<main>
|
<main>
|
||||||
<h1>Welcome to digimarks, your online bookmarking and notes tool</h1>
|
<h1>Welcome to digimarks, your online bookmarking and notes tool</h1>
|
||||||
|
|
||||||
<p>Please visit your personal url, or <a href="https://github.com/aquatix/digimarks">see the digimarks
|
<p>Please visit your personal url, or <a href="https://codeberg.org/diginaut/digimarks">see the digimarks
|
||||||
project page</a>.</p>
|
project page</a>.</p>
|
||||||
|
|
||||||
<p>If you forgot/lost your personal url, contact your digimarks
|
<p>If you forgot/lost your personal url, contact your digimarks
|
||||||
|
|||||||
@@ -185,12 +185,17 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
#}
|
#}
|
||||||
<form method="dialog" id="bookmarkEditForm" x-if="$store.digimarks.bookmarkToEdit">
|
<template x-if="$store.digimarks.bookmarkToEditVisible">
|
||||||
|
<form method="dialog" id="bookmarkEditForm">
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
<label for="bookmark_url">URL</label>
|
<label for="bookmark_url">URL</label>
|
||||||
<input id="bookmark_url" type="text" name="bookmark_url" placeholder="url"
|
<input id="bookmark_url" type="text" name="bookmark_url" placeholder="url"
|
||||||
x-on:change.debounce="$store.digimarks.bookmarkURLChanged()"
|
x-on:change.debounce="$store.digimarks.bookmarkURLChanged()"
|
||||||
x-model="$store.digimarks.bookmarkToEdit.url">
|
x-model="$store.digimarks.bookmarkToEdit.url">
|
||||||
|
<p x-show="$store.digimarks.bookmarkToEdit.http_status > 202"
|
||||||
|
x-text="'HTTP statuscode: ' + $store.digimarks.bookmarkToEdit.http_status" x-cloak
|
||||||
|
class="error"></p>
|
||||||
|
<p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
<label for="bookmark_title">Title</label>
|
<label for="bookmark_title">Title</label>
|
||||||
@@ -210,18 +215,21 @@
|
|||||||
placeholder="tags, divided bij comma's"
|
placeholder="tags, divided bij comma's"
|
||||||
x-model="$store.digimarks.bookmarkToEdit.tags">
|
x-model="$store.digimarks.bookmarkToEdit.tags">
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<p x-show="$store.digimarks.bookmarkToEditError" x-data="$store.digimarks.bookmarkToEditError"></p>
|
<p x-show="$store.digimarks.bookmarkToEditError"
|
||||||
|
x-text="$store.digimarks.bookmarkToEditError" x-cloak class="error"></p>
|
||||||
<p>
|
<p>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="strip" id="strip"/>
|
<input type="checkbox" x-model="$store.digimarks.bookmarkToEdit.strip_params"/>
|
||||||
<span>Strip parameters from url (like <em>?utm_source=social</em> - can break the link!)</span>
|
<span>Strip parameters from url (like <em>?utm_source=social</em> - can break the link!)</span>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<button value="cancel">Cancel</button>
|
<button value="cancel">Cancel</button>
|
||||||
<button @click="$store.digimarks.saveBookmark()">Save</button>
|
<button @click="$store.digimarks.saveBookmark()">Save</button>
|
||||||
|
<button @click="$store.digimarks.addBookmark()">Add</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</template>
|
||||||
</dialog>
|
</dialog>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user