Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bukuserver] Bookmarklet, optional fetch, refactor #643

Merged
merged 6 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ buku.py
/tests/test_bukuDb/places.sqlite*
/tests/vcr_cassettes/test_search_and_open_all_in_browser.yaml
/tests/vcr_cassettes/tests.test_bukuDb/
/bookmarks.db
9 changes: 6 additions & 3 deletions bukuserver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ See more option on `bukuserver run --help` and `bukuserver --help`

### Configuration

Following are available os env config available for bukuserver.
The following are os env config variables available for bukuserver.

| Name (without prefix) | Description | Value |
| Name (_without prefix_) | Description | Value |
| --- | --- | --- |
| PER_PAGE | bookmarks per page | positive integer [default: 10] |
| SECRET_KEY | [flask secret key](https://flask.palletsprojects.com/config/#SECRET_KEY) | string [default: os.urandom(24)] |
Expand All @@ -123,8 +123,11 @@ Following are available os env config available for bukuserver.
| DISABLE_FAVICON | disable bookmark [favicons](https://wikipedia.org/wiki/Favicon) | boolean [default: `true`] |
| OPEN_IN_NEW_TAB | url link open in new tab | boolean [default: `false`] |
| REVERSE_PROXY_PATH | reverse proxy path | string |
| LOCALE | GUI language (partial support) | string [default: `en`] |

Note: `BUKUSERVER_` is the common prefix.
Note: `BUKUSERVER_` is the common prefix (_every variable starts with it_).

Note: Valid boolean values are `true`, `false`, `1`, `0` (case-insensitive).

Note: if input is invalid, the default value will be used if defined

Expand Down
290 changes: 290 additions & 0 deletions bukuserver/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
#!/usr/bin/env python
# pylint: disable=wrong-import-order, ungrouped-imports
"""Server module."""
import collections
import typing as T
from typing import Any, Dict, Union # NOQA; type: ignore
from unittest import mock

from flask.views import MethodView
from flask_api import exceptions, status

import buku
from buku import BukuDb

import flask
from flask import current_app, jsonify, redirect, request, url_for

try:
from . import forms, response
except ImportError:
from bukuserver import forms, response


STATISTIC_DATA = None

response_ok = lambda: (jsonify(response.response_template['success']),
status.HTTP_200_OK,
{'ContentType': 'application/json'})
response_bad = lambda: (jsonify(response.response_template['failure']),
status.HTTP_400_BAD_REQUEST,
{'ContentType': 'application/json'})
to_response = lambda ok: response_ok() if ok else response_bad()


def get_bukudb():
"""get bukudb instance"""
db_file = current_app.config.get('BUKUSERVER_DB_FILE', None)
return BukuDb(dbfile=db_file)


def search_tag(
db: BukuDb, stag: T.Optional[str] = None, limit: T.Optional[int] = None
) -> T.Tuple[T.List[str], T.Dict[str, int]]:
"""search tag.

db:
buku db instance
stag:
search tag
limit:
positive integer limit

Returns
-------
tuple
list of unique tags sorted alphabetically and dictionary of tag and its usage count

Raises
------
ValueError
if limit is not positive
"""
if limit is not None and limit < 1:
raise ValueError("limit must be positive")
tags: T.Set[str] = set()
counter = collections.Counter()
query_list = ["SELECT DISTINCT tags , COUNT(tags) FROM bookmarks"]
if stag:
query_list.append("where tags LIKE :search_tag")
query_list.append("GROUP BY tags")
row: T.Tuple[str, int]
for row in db.cur.execute(" ".join(query_list), {"search_tag": f"%{stag}%"}):
for tag in row[0].strip(buku.DELIM).split(buku.DELIM):
if not tag:
continue
tags.add(tag)
counter[tag] += row[1]
return list(sorted(tags)), dict(counter.most_common(limit))


class ApiTagView(MethodView):

def get(self, tag: T.Optional[str]):
bukudb = get_bukudb()
if tag is None:
return {"tags": search_tag(db=bukudb, limit=5)[0]}
tags = search_tag(db=bukudb, stag=tag)
if tag not in tags[1]:
raise exceptions.NotFound()
return dict(name=tag, usage_count=tags[1][tag])

def put(self, tag: str):
bukudb = get_bukudb()
try:
new_tags = request.data.get('tags') # type: ignore
if new_tags:
new_tags = new_tags.split(',')
else:
return response_bad()
except AttributeError as e:
raise exceptions.ParseError(detail=str(e))
return to_response(bukudb.replace_tag(tag, new_tags))


class ApiBookmarkView(MethodView):

def get(self, rec_id: Union[int, None]):
if rec_id is None:
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
all_bookmarks = bukudb.get_rec_all()
result = {'bookmarks': []} # type: Dict[str, Any]
for bookmark in all_bookmarks:
result_bookmark = {
'url': bookmark[1],
'title': bookmark[2],
'tags': [x for x in bookmark[3].split(',') if x],
'description': bookmark[4]
}
if not request.path.startswith('/api/'):
result_bookmark['id'] = bookmark[0]
result['bookmarks'].append(result_bookmark)
res = jsonify(result)
else:
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
bookmark = bukudb.get_rec_by_id(rec_id)
if bookmark is None:
res = response_bad()
else:
res = jsonify({
'url': bookmark[1],
'title': bookmark[2],
'tags': [x for x in bookmark[3].split(',') if x],
'description': bookmark[4]
})
return res

def post(self, rec_id: None = None):
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
create_bookmarks_form = forms.ApiBookmarkForm()
url_data = create_bookmarks_form.url.data
result_flag = bukudb.add_rec(
url_data,
create_bookmarks_form.title.data,
create_bookmarks_form.tags.data,
create_bookmarks_form.description.data
)
return to_response(result_flag != -1)

def put(self, rec_id: int):
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
result_flag = bukudb.update_rec(
rec_id,
request.form.get('url'),
request.form.get('title'),
request.form.get('tags'),
request.form.get('description'))
return to_response(result_flag)

def delete(self, rec_id: Union[int, None]):
if rec_id is None:
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
with mock.patch('buku.read_in', return_value='y'):
result_flag = bukudb.cleardb()
else:
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
result_flag = bukudb.delete_rec(rec_id)
return to_response(result_flag)


class ApiBookmarkRangeView(MethodView):

def get(self, starting_id: int, ending_id: int):
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
max_id = bukudb.get_max_id()
if starting_id > max_id or ending_id > max_id:
return response_bad()
result = {'bookmarks': {}} # type: ignore
for i in range(starting_id, ending_id + 1, 1):
bookmark = bukudb.get_rec_by_id(i)
result['bookmarks'][i] = {
'url': bookmark[1],
'title': bookmark[2],
'tags': [x for x in bookmark[3].split(',') if x],
'description': bookmark[4]
}
return jsonify(result)

def put(self, starting_id: int, ending_id: int):
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
max_id = bukudb.get_max_id()
if starting_id > max_id or ending_id > max_id:
return response_bad()
for i in range(starting_id, ending_id + 1, 1):
updated_bookmark = request.data.get(str(i)) # type: ignore
result_flag = bukudb.update_rec(
i,
updated_bookmark.get('url'),
updated_bookmark.get('title'),
updated_bookmark.get('tags'),
updated_bookmark.get('description'))
if result_flag is False:
return response_bad()
return response_ok()

def delete(self, starting_id: int, ending_id: int):
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
max_id = bukudb.get_max_id()
if starting_id > max_id or ending_id > max_id:
return response_bad()
idx = min([starting_id, ending_id])
result_flag = bukudb.delete_rec(idx, starting_id, ending_id, is_range=True)
return to_response(result_flag)


class ApiBookmarkSearchView(MethodView):

def get(self):
arg_obj = request.args
keywords = arg_obj.getlist('keywords')
all_keywords = arg_obj.get('all_keywords')
deep = arg_obj.get('deep')
regex = arg_obj.get('regex')
# api request is more strict
all_keywords = False if all_keywords is None else all_keywords
deep = False if deep is None else deep
regex = False if regex is None else regex
all_keywords = (
all_keywords if isinstance(all_keywords, bool) else
all_keywords.lower() == 'true'
)
deep = deep if isinstance(deep, bool) else deep.lower() == 'true'
regex = regex if isinstance(regex, bool) else regex.lower() == 'true'

result = {'bookmarks': []}
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
found_bookmarks = bukudb.searchdb(keywords, all_keywords, deep, regex)
found_bookmarks = [] if found_bookmarks is None else found_bookmarks
res = None
if found_bookmarks is not None:
for bookmark in found_bookmarks:
result_bookmark = {
'id': bookmark[0],
'url': bookmark[1],
'title': bookmark[2],
'tags': list(filter(lambda x: x, bookmark[3].split(','))),
'description': bookmark[4]
}
result['bookmarks'].append(result_bookmark)
current_app.logger.debug('total bookmarks:{}'.format(len(result['bookmarks'])))
res = jsonify(result)
return res

def delete(self):
arg_obj = request.form
keywords = arg_obj.getlist('keywords')
all_keywords = arg_obj.get('all_keywords')
deep = arg_obj.get('deep')
regex = arg_obj.get('regex')
# api request is more strict
all_keywords = False if all_keywords is None else all_keywords
deep = False if deep is None else deep
regex = False if regex is None else regex
all_keywords = (
all_keywords if isinstance(all_keywords, bool) else
all_keywords.lower() == 'true'
)
deep = deep if isinstance(deep, bool) else deep.lower() == 'true'
regex = regex if isinstance(regex, bool) else regex.lower() == 'true'
bukudb = getattr(flask.g, 'bukudb', get_bukudb())
found_bookmarks = bukudb.searchdb(keywords, all_keywords, deep, regex)
found_bookmarks = [] if found_bookmarks is None else found_bookmarks
res = None
if found_bookmarks is not None:
for bookmark in found_bookmarks:
if not bukudb.delete_rec(bookmark[0]):
res = response_bad()
return res or response_ok()


class BookmarkletView(MethodView): # pylint: disable=too-few-public-methods
def get(self):
url = request.args.get('url')
title = request.args.get('title')
description = request.args.get('description')

bukudb = getattr(flask.g, 'bukudb', get_bukudb())
rec_id = bukudb.get_rec_id(url)
if rec_id >= 0:
return redirect(url_for('bookmark.edit_view', id=rec_id))
return redirect(url_for('bookmark.create_view', link=url, title=title, description=description))
2 changes: 1 addition & 1 deletion bukuserver/bookmarklet.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

var url = location.href;
var title = document.title.trim() || "";
var desc = document.getSelection().toString().trim();
var desc = document.getSelection().toString().trim() || (document.querySelector('meta[name$=description i], meta[property$=description i]')||{}).content || "";
if(desc.length > 4000){
desc = desc.substr(0,4000) + '...';
alert('The selected text is too long, it will be truncated.');
Expand Down
3 changes: 2 additions & 1 deletion bukuserver/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ class HomeForm(SearchBookmarksForm):


class BookmarkForm(FlaskForm):
url = wtforms.StringField('Url', name='link', validators=[wtforms.validators.DataRequired()])
url = wtforms.StringField('Url', name='link', validators=[wtforms.validators.InputRequired()])
title = wtforms.StringField()
tags = wtforms.StringField()
description = wtforms.TextAreaField()
fetch = wtforms.HiddenField(filters=[bool])

class ApiBookmarkForm(BookmarkForm):
url = wtforms.StringField(validators=[wtforms.validators.DataRequired()])
Loading