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
Show file tree
Hide file tree
Changes from 9 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 @@ -172,4 +172,5 @@ app.build/
app.dist/
config/
release/
appdata/
ann.md
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

"""
Expand Down
7 changes: 5 additions & 2 deletions api/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -33,7 +34,8 @@ def local_cover_search(title: str, artist: str, album: str):
if res.status_code == 200:
return res.content, 200, {"Content-Type": res.headers['Content-Type']}

@app.route('/cover', methods=['GET'])
@app.route('/cover', methods=['GET'], endpoint='cover_endpoint')
@require_auth_decorator(permission='rw')
@cache.cached(timeout=86400, key_prefix=make_cache_key)
@no_error(exceptions=AttributeError)
def cover_api():
Expand All @@ -54,7 +56,8 @@ def cover_api():
abort(500, '服务存在错误,暂时无法查询')


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

import re
import sqlite3
from datetime import datetime, timezone
from flask import request, jsonify
from mod.auth import require_auth_decorator

from mod.db import SqliteDict, saved_path

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
return True, "OK", 200


def kv_set(table_name: str, para: dict) -> tuple[bool, str|dict, int]:
"""
写入或更新k-v数据
"""
check_status: tuple[bool, str, int] = valide_tablename(table_name)
if not check_status[0]:
return check_status
kv_list: dict[str, str] = para.get("data")
results: dict = {}
with SqliteDict(tablename=table_name) as db:
for key, value in kv_list.items():
try:
db[key] = value
db.commit()
results[key] = {
"status": "Success",
"timezone": int(datetime.now(timezone.utc).timestamp()),
}
except Exception as e:
results[key] = {
"status": "Error",
"Message": e,
"timezone": int(datetime.now(timezone.utc).timestamp()),
}
return True, results, 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


def custom_sql(sql: str) -> list[dict]:
with sqlite3.connect(saved_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(sql)
rows: list = cursor.fetchall()
return [dict(row) for row in rows]


@v1_bp.route("/db/<path:table_name>", methods=["POST", "PUT", "GET", "DELETE"], endpoint='db_set_endpoint')
@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":
if request.method == "POST" or request.method == "PUT":
status, message, code = kv_set(table_name, para)
return {"code": code, "message": message}, code
elif request.method == "GET":
status, message, code = kv_get(table_name, para)
if status:
return message, 200
else:
return {"code": code, "message": message}, code
elif request.method == "DELETE":
Copy link

Choose a reason for hiding this comment

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

改动和删除数据不也敏感吗(

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


@v1_bp.route("/db", methods=["POST"], endpoint='db_custom')
def db_custom():
"""
执行自定义的SQL
返回json结果
此操作敏感,因此必须有可信授权
用户必须保证使用者受到信任
"""
para: dict = request.json
if not para or not (sql := para.get('sql')):
logger.warning("The request submitted by the client lacks necessary parameters")
return jsonify({
"status": "Error",
"code": 400,
"timezone": int(datetime.now(timezone.utc).timestamp()),
"message": "Missing 'sql' parameter"
}), 400

results = []
for s in sql:
try:
result: list[dict] = custom_sql(sql=s)
results.append({
"sql": s,
"status": "Success",
"code": 200,
"timezone": int(datetime.now(timezone.utc).timestamp()),
"result": result
})
except sqlite3.Error as e:
logger.error(f"SQLite error during custom SQL execution: {str(e)}")
results.append({
"sql": s,
"status": "Error",
"code": 500,
"timezone": int(datetime.now(timezone.utc).timestamp()),
"message": f"SQLite error: {str(e)}"
})
except Exception as e:
logger.error(f"Server error during custom SQL execution: {str(e)}")
results.append({
"sql": s,
"status": "Error",
"code": 500,
"timezone": int(datetime.now(timezone.utc).timestamp()),
"message": f"Server error: {str(e)}"
})

return jsonify(results)
29 changes: 8 additions & 21 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
Expand Down Expand Up @@ -42,13 +41,9 @@ def download(self):
self.file.write(chunk)


@v1_bp.route("/file/download", methods=["POST"])
@v1_bp.route("/file/download", methods=["POST"], endpoint='file_api_download_endpoint')
@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
Expand All @@ -68,13 +63,9 @@ def file_api_download():
return {"error": str(e), "code": 500}, 500


@v1_bp.route('/file/upload', methods=['POST'])
@v1_bp.route('/file/upload', methods=['POST'], endpoint='upload_file_endpoint')
@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

Expand Down Expand Up @@ -104,13 +95,9 @@ def upload_file():
return {"code": 200}, 200


@v1_bp.route('/file/list', methods=['GET'])
@v1_bp.route('/file/list', methods=['GET'], endpoint='list_file_endpoint')
@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)
Expand Down
27 changes: 10 additions & 17 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]):
Expand All @@ -23,15 +24,11 @@ def read_file_with_encoding(file_path: str, encodings: list[str]):
return None


@app.route('/lyrics', methods=['GET'])
@v1_bp.route('/lyrics/single', methods=['GET'])
@app.route('/lyrics', methods=['GET'], endpoint='lyrics_endpoint')
@v1_bp.route('/lyrics/single', methods=['GET'], endpoint='lyrics_endpoint')
@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, "请携带参数访问")
Expand Down Expand Up @@ -62,15 +59,11 @@ def lyrics():
return "Lyrics not found.", 404


@app.route('/jsonapi', methods=['GET'])
@v1_bp.route('/lyrics/advance', methods=['GET'])
@app.route('/jsonapi', methods=['GET'], endpoint='jsonapi_endpoint')
@v1_bp.route('/lyrics/advance', methods=['GET'], endpoint='jsonapi_endpoint')
@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', ''))
Expand Down
Loading
Loading