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

Fixed #90 实现kv数据库接口 #93

Merged
merged 11 commits into from
Nov 9, 2024
Merged
Prev Previous commit
Next Next commit
实现KV数据库API(sqlite后端)
HisAtri committed Oct 27, 2024
commit c6503607e862711f37acd4e7b9e2adbb7114cb5d
2 changes: 1 addition & 1 deletion api/__import__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from . import cover, login, lyrics, source, tag, time, file
from . import cover, login, lyrics, source, tag, time, file, db
from . import waf

"""
3 changes: 3 additions & 0 deletions api/cover.py
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
from flask import request, abort, redirect
from urllib.parse import unquote_plus
from mygo.devtools import no_error
from mod.auth import require_auth_decorator

from mod import searchx

@@ -34,6 +35,7 @@ def local_cover_search(title: str, artist: str, album: str):
return res.content, 200, {"Content-Type": res.headers['Content-Type']}

@app.route('/cover', methods=['GET'])
@require_auth_decorator(permission='rw')
@cache.cached(timeout=86400, key_prefix=make_cache_key)
@no_error(exceptions=AttributeError)
def cover_api():
@@ -55,6 +57,7 @@ def cover_api():


@v1_bp.route('/cover/<path:s_type>', methods=['GET'])
@require_auth_decorator(permission='r')
@cache.cached(timeout=86400, key_prefix=make_cache_key)
@no_error(exceptions=AttributeError)
def cover_new(s_type):
168 changes: 168 additions & 0 deletions api/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
from . import *

import re
from flask import request
from mod.auth import require_auth_decorator

from mod.db import SqliteDict

SQLITE_RESERVED_WORDS = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

好像并没有用于限制CREATE TABLE,检查这个好像不是特别有用(

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

好像确实表名没那么多限制,引号加一个就行

"ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT",
"BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT",
"CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE",
"DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DROP", "EACH", "ELSE", "END",
"ESCAPE", "EXCEPT", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", "FOR", "FOREIGN", "FROM", "FULL", "GLOB",
"GROUP", "HAVING", "IF", "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT",
"INSTEAD", "INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "KEY", "LEFT", "LIKE", "LIMIT", "MATCH", "NATURAL",
"NO", "NOT", "NOTNULL", "NULL", "OF", "OFFSET", "ON", "OR", "ORDER", "OUTER", "PLAN", "PRAGMA", "PRIMARY",
"QUERY", "RAISE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", "RENAME", "REPLACE", "RESTRICT",
"RIGHT", "ROLLBACK", "ROW", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", "TO", "TRANSACTION",
"TRIGGER", "UNION", "UNIQUE", "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", "WITH",
"WITHOUT"
}


def valide_tablename(table_name: str) -> tuple[bool, str, int]:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate

if not table_name:
return False, "Missing table_name.", 422
invalid_chars = re.compile(r"[^a-zA-Z0-9_]") # 表名仅允许包含字母、数字和下划线
if invalid_chars.search(table_name):
return False, "Invalid table_name: contains invalid characters.", 422
if table_name.upper() in SQLITE_RESERVED_WORDS:
return False, "Invalid table_name: is a reserved keyword.", 422
# 限制表名长度为64字符
if len(table_name) > 64:
return False, "Invalid table_name: too long.", 422


def kv_set(table_name: str, para: dict) -> tuple[bool, str, int]:
"""
写入或更新k-v数据
"""
check_status: tuple[bool, str, int] = valide_tablename(table_name)
if not check_status[0]:
return check_status
key = para.get("key")
if not key:
return False, "Missing key.", 422
elif type(key) is not str:
return False, "Invalid key: must be a string.", 422
value = para.get("value")
if not value:
return False, "Missing value.", 422
try:
with SqliteDict(tablename=table_name) as db:
db[key] = value
db.commit()
except Exception as e:
return False, str(e), 500
return True, table_name, 200

def kv_get(table_name: str, para: dict) -> tuple[bool, any, int]:
"""
读取k-v数据
"""
check_status: tuple[bool, str, int] = valide_tablename(table_name)
if not check_status[0]:
return check_status
key = para.get("key")
if not key:
return False, "Missing key.", 422
elif type(key) is not str:
return False, "Invalid key: must be a string.", 422
try:
with SqliteDict(tablename=table_name) as db:
return True, db[key], 200
except KeyError:
return False, "Key not found.", 404
except Exception as e:
return False, str(e), 500

def kv_del(table_name: str, para: dict) -> tuple[bool, any, int]:
"""
删除k-v数据
"""
check_status: tuple[bool, str, int] = valide_tablename(table_name)
if not check_status[0]:
return check_status
key = para.get("key")
if not key:
return False, "Missing key.", 422
elif type(key) is not str:
return False, "Invalid key: must be a string.", 422

try:
with SqliteDict(tablename=table_name) as db:
del db[key]
db.commit()
return True, key, 200
except KeyError:
return False, "Key not found.", 404
except Exception as e:
return False, str(e), 500

@v1_bp.route("/db/<path:table_name>", methods=["POST", "PUT"])
@require_auth_decorator(permission='rw')
def db_set(table_name):
"""
写入或更新k-v数据
"""
para: dict = request.json
if not para:
return {"code": 422, "message": "Missing JSON."}, 422

type = para.get("type")
if not type:
return {"code": 422, "message": "Missing type."}, 422
match type:
case "kv":
status, message, code = kv_set(table_name, para)
return {"code": code, "message": message}, code
case _:
return {"code": 422, "message": "Invalid type."}, 422

@v1_bp.route("/db/<path:table_name>", methods=["GET"])
@require_auth_decorator(permission='rw')
def db_get(table_name):
"""
读取k-v数据
"""
para: dict = request.json
if not para:
return {"code": 422, "message": "Missing JSON."}, 422

type = para.get("type")
if not type:
return {"code": 422, "message": "Missing type."}, 422
match type:
case "kv":
status, message, code = kv_get(table_name, para)
if status:
return message, 200
else:
return {"code": code, "message": message}, code
case _:
return {"code": 422, "message": "Invalid type."}, 422

@v1_bp.route("/db/<path:table_name>", methods=["DELETE"])
@require_auth_decorator(permission='rw')
def db_del(table_name):
"""
删除k-v数据
"""
para: dict = request.json
if not para:
return {"code": 422, "message": "Missing JSON."}, 422

type = para.get("type")
if not type:
return {"code": 422, "message": "Missing type."}, 422
match type:
case "kv":
status, message, code = kv_del(table_name, para)
if status:
return message, 200
else:
return {"code": code, "message": message}, code
case _:
return {"code": 422, "message": "Invalid type."}, 422
23 changes: 5 additions & 18 deletions api/file.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import hashlib

from mod.auth import webui
from mod.auth.authentication import require_auth
from mod.auth import require_auth_decorator
from . import *

import os
import requests
from urllib.parse import urlparse
from flask import request, render_template_string, send_from_directory
from flask import request
from werkzeug.utils import secure_filename

from mod.tools import calculate_md5
@@ -43,12 +42,8 @@ def download(self):


@v1_bp.route("/file/download", methods=["POST"])
@require_auth_decorator(permission='rwd')
def file_api_download():
match require_auth(request=request, permission="rwd"):
case -1:
return render_template_string(webui.error()), 403
case -2:
return render_template_string(webui.error()), 421
data = request.json
if not data:
return {"error": "invalid request body", "code": 400}, 400
@@ -69,12 +64,8 @@ def file_api_download():


@v1_bp.route('/file/upload', methods=['POST'])
@require_auth_decorator(permission='rwd')
def upload_file():
match require_auth(request=request, permission="rwd"):
case -1:
return render_template_string(webui.error()), 403
case -2:
return render_template_string(webui.error()), 421
if 'file' not in request.files:
return {"error": "No file part in the request", "code": 400}, 400

@@ -105,12 +96,8 @@ def upload_file():


@v1_bp.route('/file/list', methods=['GET'])
@require_auth_decorator(permission='rwd')
def list_file():
match require_auth(request=request, permission="rwd"):
case -1:
return render_template_string(webui.error()), 403
case -2:
return render_template_string(webui.error()), 421
path = request.args.get('path', os.getcwd())
row = request.args.get('row', 500)
page = request.args.get('page', 1)
19 changes: 6 additions & 13 deletions api/lyrics.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
from mygo.devtools import no_error

from . import *

import os

from flask import request, abort, jsonify, render_template_string
from flask import request, abort, jsonify
from urllib.parse import unquote_plus

from mod import lrc
from mod import searchx
from mod import tools
from mod import tag
from mod.auth import webui
from mod.auth.authentication import require_auth
from mod.auth import require_auth_decorator


def read_file_with_encoding(file_path: str, encodings: list[str]):
@@ -25,13 +26,9 @@ def read_file_with_encoding(file_path: str, encodings: list[str]):

@app.route('/lyrics', methods=['GET'])
@v1_bp.route('/lyrics/single', methods=['GET'])
@require_auth_decorator(permission='r')
@cache.cached(timeout=86400, key_prefix=make_cache_key)
def lyrics():
match require_auth(request=request):
case -1:
return render_template_string(webui.error()), 403
case -2:
return render_template_string(webui.error()), 421
# 通过request参数获取文件路径
if not bool(request.args):
abort(404, "请携带参数访问")
@@ -64,13 +61,9 @@ def lyrics():

@app.route('/jsonapi', methods=['GET'])
@v1_bp.route('/lyrics/advance', methods=['GET'])
@require_auth_decorator(permission='r')
@cache.cached(timeout=86400, key_prefix=make_cache_key)
def lrc_json():
match require_auth(request=request):
case -1:
return render_template_string(webui.error()), 403
case -2:
return render_template_string(webui.error()), 421
if not bool(request.args):
abort(404, "请携带参数访问")
path = unquote_plus(request.args.get('path', ''))
12 changes: 3 additions & 9 deletions api/source.py
Original file line number Diff line number Diff line change
@@ -2,10 +2,9 @@

import os

from flask import request, abort, redirect, send_from_directory, render_template_string
from flask import abort, redirect, send_from_directory

from mod.auth import webui
from mod.auth.authentication import require_auth
from mod.auth import require_auth_decorator


@app.route('/')
@@ -59,18 +58,13 @@ def serve_file(filename):

@app.route('/file/<path:filename>')
@v1_bp.route('/file/<path:filename>')
@require_auth_decorator(permission='r')
def file_viewer(filename):
"""
文件查看器
:param filename:
:return:
"""
# 需要权限
match require_auth(request=request):
case -1:
return render_template_string(webui.error()), 403
case -2:
return render_template_string(webui.error()), 421
# 拓展名白名单
ALLOWED_EXTENSIONS = ('.mp3', '.flac', '.wav', '.ape', '.ogg', '.m4a', '.aac', '.wma', '.mp4', '.m4p', '.m4b',
'txt', 'lrc', 'webp', 'jpg', 'jpeg', 'png', 'bmp', 'gif', 'webp', 'svg', 'ico', 'mp4', 'webm',
15 changes: 3 additions & 12 deletions api/tag.py
Original file line number Diff line number Diff line change
@@ -2,26 +2,17 @@

from . import *

from flask import request, render_template_string, abort
from flask import request

from mod import tag
from mod.auth import webui
from mod.auth.authentication import require_auth
from mod.auth import require_auth_decorator
from mod.dev.debugger import debugger


@app.route('/tag', methods=['POST', 'PUT'])
@app.route('/confirm', methods=['POST', 'PUT'])
@require_auth_decorator(permission='rw')
def set_tag():
match require_auth(request=request, permission='rw'):
case -1:
logger.error("Unauthorized access: 未经授权的用户请求修改标签")
return render_template_string(webui.error()), 403
case -2:
logger.error("Unauthorized access: 您没有为API设置鉴权功能,为了安全起见,有关本地文件修改的功能无法使用。"
"具体请查看<https://docs.lrc.cx/docs/deploy/auth>以启用API鉴权功能。")
return render_template_string(webui.error()), 421

music_data = request.json
audio_path = music_data.get("path")
if not audio_path:
Loading