Compare commits
11 Commits
6626fac452
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f4ca52a10 | |||
| 62c33c47c6 | |||
| 692c0794c5 | |||
| 848c4b8fd8 | |||
| 1a160c9415 | |||
| 85d8124a0c | |||
| 84494827ad | |||
| 35c8934963 | |||
| cf247231e4 | |||
| 0186a95dd4 | |||
| fa855bc7bb |
@@ -2,10 +2,6 @@ FROM python:3.12-slim
|
|||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
#RUN apt-get update &&\
|
|
||||||
# apt-get install -y default-mysql-client &&\
|
|
||||||
# apt-get clean &&\
|
|
||||||
# rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY requirements.txt ./requirements.txt
|
COPY requirements.txt ./requirements.txt
|
||||||
RUN pip install --no-cache-dir -r ./requirements.txt
|
RUN pip install --no-cache-dir -r ./requirements.txt
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import base64
|
|
||||||
import os
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from datetime import datetime, UTC
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
from flask import jsonify, Blueprint, request, make_response
|
from flask import jsonify, Blueprint, request, make_response
|
||||||
from flask_limiter import Limiter
|
from flask_limiter import Limiter
|
||||||
from flask_limiter.util import get_remote_address
|
from flask_limiter.util import get_remote_address
|
||||||
from jwt import decode, ExpiredSignatureError, InvalidTokenError, get_unverified_header
|
from jwt import decode, ExpiredSignatureError, InvalidTokenError, get_unverified_header
|
||||||
import importlib
|
|
||||||
import requests
|
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
from db.models.APIKey import APIKey
|
||||||
|
from db import get_db
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
import env_provider
|
import env_provider
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import importlib
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
_public_key_cache = {}
|
_public_key_cache = {}
|
||||||
_lock = Lock()
|
_lock = Lock()
|
||||||
@@ -80,24 +82,59 @@ def verify_token(token):
|
|||||||
print(e)
|
print(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def is_user_admin():
|
||||||
|
is_admin = False
|
||||||
|
auth_header = request.headers.get('Authorization')
|
||||||
|
if auth_header and auth_header.startswith('Bearer'):
|
||||||
|
token = auth_header.split(" ")[1]
|
||||||
|
decoded = verify_token(token)
|
||||||
|
if decoded:
|
||||||
|
user_roles = decoded.get("resource_access", {}).get(env_provider.KC_CLIENT_ID, {}).get("roles", [])
|
||||||
|
is_admin = 'admin' in user_roles
|
||||||
|
return is_admin
|
||||||
|
|
||||||
def require_auth(roles=[]):
|
def require_auth(roles=[]):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if request.method == "OPTIONS":
|
if request.method == "OPTIONS":
|
||||||
return '', 200
|
return '', 200
|
||||||
|
|
||||||
auth_header = request.headers.get('Authorization')
|
auth_header = request.headers.get('Authorization')
|
||||||
|
api_key_header = request.headers.get('X-API-Key')
|
||||||
|
|
||||||
|
if auth_header and api_key_header:
|
||||||
|
return jsonify({"error": "Cannot use both Bearer token and API Key authentication"}), 403
|
||||||
|
|
||||||
|
if api_key_header:
|
||||||
|
api_key = get_api_key(api_key_header)
|
||||||
|
if not api_key:
|
||||||
|
return jsonify({"error": "Invalid API key"}), 401
|
||||||
|
|
||||||
|
expire_time = api_key.expire.replace(tzinfo=UTC) if api_key.expire.tzinfo is None else api_key.expire
|
||||||
|
if datetime.now(UTC) > expire_time:
|
||||||
|
return jsonify({"error": "API key has expired"}), 401
|
||||||
|
|
||||||
|
if roles and not (set(roles) & set(api_key.roles)):
|
||||||
|
return jsonify({"error": "Forbidden, permission denied"}), 403
|
||||||
|
|
||||||
|
update_last_used(api_key)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
if not auth_header or not auth_header.startswith('Bearer'):
|
if not auth_header or not auth_header.startswith('Bearer'):
|
||||||
return jsonify({"error": "Unauthorized"}), 401
|
return jsonify({"error": "Unauthorized"}), 401
|
||||||
|
|
||||||
token = auth_header.split(" ")[1]
|
token = auth_header.split(" ")[1]
|
||||||
|
|
||||||
decoded = verify_token(token)
|
decoded = verify_token(token)
|
||||||
if not decoded:
|
if not decoded:
|
||||||
return jsonify({"error": "Invalid or expired token"}), 401
|
return jsonify({"error": "Invalid or expired token"}), 401
|
||||||
|
|
||||||
user_roles = decoded.get("resource_access", {}).get(env_provider.KC_CLIENT_ID, {}).get("roles", [])
|
user_roles = decoded.get("resource_access", {}).get(env_provider.KC_CLIENT_ID, {}).get("roles", [])
|
||||||
if roles and not (set(roles) & set(user_roles)):
|
if roles and not (set(roles) & set(user_roles)):
|
||||||
print("auth failed")
|
print("auth failed")
|
||||||
return jsonify({"error": "Forbidden, permission denied"}), 403
|
return jsonify({"error": "Forbidden, permission denied"}), 403
|
||||||
|
|
||||||
print("auth success")
|
print("auth success")
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
@@ -126,17 +163,18 @@ limiter = Limiter(
|
|||||||
|
|
||||||
|
|
||||||
def register_blueprints(app):
|
def register_blueprints(app):
|
||||||
current_dir = os.path.dirname(__file__)
|
|
||||||
for filename in os.listdir(current_dir):
|
|
||||||
if filename == "__init__.py" or not filename.endswith(".py"):
|
|
||||||
continue
|
|
||||||
module_name = filename[:-3]
|
|
||||||
module = importlib.import_module(f"api.{module_name}")
|
|
||||||
for attr in dir(module):
|
|
||||||
bp = getattr(module, attr)
|
|
||||||
if isinstance(bp, Blueprint):
|
|
||||||
app.register_blueprint(bp)
|
|
||||||
|
|
||||||
|
package_name = __name__
|
||||||
|
package_path = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
for finder, mod_name, is_pkg in pkgutil.walk_packages([package_path], package_name + "."):
|
||||||
|
module = importlib.import_module(mod_name)
|
||||||
|
for attr_name in dir(module):
|
||||||
|
item = getattr(module, attr_name)
|
||||||
|
if isinstance(item, Blueprint):
|
||||||
|
if item.name in app.blueprints:
|
||||||
|
continue
|
||||||
|
app.register_blueprint(item)
|
||||||
|
|
||||||
|
|
||||||
def generate_etag(data):
|
def generate_etag(data):
|
||||||
@@ -159,3 +197,16 @@ def etag_response(f):
|
|||||||
return response
|
return response
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def generate_api_key(length=32):
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||||
|
|
||||||
|
def get_api_key(key):
|
||||||
|
with get_db() as session:
|
||||||
|
return session.query(APIKey).filter_by(key=key, is_active=True).first()
|
||||||
|
|
||||||
|
def update_last_used(api_key):
|
||||||
|
with get_db() as session:
|
||||||
|
api_key.last_used_at = datetime.now(UTC)
|
||||||
|
session.commit()
|
||||||
36
api/apikey/__init__.py
Normal file
36
api/apikey/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from api import get_api_key, generate_api_key
|
||||||
|
from db import get_db
|
||||||
|
from api import require_auth
|
||||||
|
from db.models.APIKey import APIKey
|
||||||
|
|
||||||
|
api_key_bp = Blueprint('apikey', __name__, url_prefix='/api/apikey')
|
||||||
|
|
||||||
|
@api_key_bp.route('/', methods=['POST'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def create_key():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data or 'name' not in data:
|
||||||
|
return jsonify({"error": "Name is required"}), 400
|
||||||
|
roles = data.get('roles', [])
|
||||||
|
try:
|
||||||
|
with get_db() as session:
|
||||||
|
apikey = APIKey(key=generate_api_key(),name=data['name'], roles=roles)
|
||||||
|
session.add(apikey);
|
||||||
|
session.commit
|
||||||
|
return jsonify(apikey.to_dict()), 201
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@api_key_bp.route('/<key>', methods=['DELETE'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def revoke_key(key):
|
||||||
|
|
||||||
|
api_key = get_api_key(key)
|
||||||
|
with get_db() as session:
|
||||||
|
if not api_key:
|
||||||
|
return jsonify({"error": "API key not found"}), 404
|
||||||
|
api_key.is_active = False
|
||||||
|
session.commit()
|
||||||
|
return jsonify({"message": "API key revoked successfully"}), 200
|
||||||
818
api/backup.py
818
api/backup.py
@@ -1,28 +1,363 @@
|
|||||||
|
"""
|
||||||
|
Backup and Restore System
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This module provides functionality for creating backups of the application's data and restoring from those backups.
|
||||||
|
|
||||||
|
Backup Structure
|
||||||
|
---------------
|
||||||
|
The backup is a zip file containing the following structure:
|
||||||
|
- Root/
|
||||||
|
- tree/ # Contains the entire path hierarchy and markdown files
|
||||||
|
- .json.meta # Metadata for the root path
|
||||||
|
- [markdown].json # Markdown files directly under root
|
||||||
|
- [folder]/ # Subfolders representing paths
|
||||||
|
- .json.meta # Metadata for this path
|
||||||
|
- [markdown].json # Markdown files in this path
|
||||||
|
- template/ # Contains markdown templates
|
||||||
|
- [template].json # One JSON file per template, using title as filename
|
||||||
|
- webhook.json # Contains all webhooks and their settings
|
||||||
|
|
||||||
|
Data Format
|
||||||
|
-----------
|
||||||
|
1. Markdown files (.json):
|
||||||
|
{
|
||||||
|
"title": "...", # Title of the markdown
|
||||||
|
"content": "...", # Content of the markdown
|
||||||
|
"created_at": "...", # Creation timestamp
|
||||||
|
"order": "...", # Order value for sorting
|
||||||
|
"shortcut": "...", # Shortcut value if any
|
||||||
|
"backup_id": ..., # Reference ID for the backup
|
||||||
|
"setting": { # Optional settings object
|
||||||
|
"permission_setting": "...", # Permission value (private, protected, etc.)
|
||||||
|
"template_setting": { # Template settings if any
|
||||||
|
"template_id": ..., # Template ID reference
|
||||||
|
"template_ref": { # Reference to the template
|
||||||
|
"title": "..." # Title of the template for lookup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2. Path metadata (.json.meta):
|
||||||
|
{
|
||||||
|
"name": "...", # Name of the path
|
||||||
|
"order": "...", # Order value for sorting
|
||||||
|
"backup_id": ..., # Reference ID for the backup
|
||||||
|
"webhook_setting": { # Optional webhook settings
|
||||||
|
"recursive": true/false, # Whether webhook applies to subpaths
|
||||||
|
"additional_header": "...", # Additional headers for webhook
|
||||||
|
"enabled": true/false, # Whether webhook is enabled
|
||||||
|
"on_events": ... # Event types that trigger the webhook
|
||||||
|
},
|
||||||
|
"webhook_ref": { # Reference to the webhook
|
||||||
|
"hook_url": "..." # URL of the webhook for lookup
|
||||||
|
},
|
||||||
|
"template_setting": { # Template settings if any
|
||||||
|
"template_id": ... # Template ID reference
|
||||||
|
},
|
||||||
|
"template_ref": { # Reference to the template
|
||||||
|
"title": "..." # Title of the template for lookup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
3. Template files (template/[name].json):
|
||||||
|
{
|
||||||
|
"title": "...", # Title of the template
|
||||||
|
"parameters": {...}, # Parameters for the template
|
||||||
|
"layout": "..." # Layout content of the template
|
||||||
|
}
|
||||||
|
|
||||||
|
4. Webhook file (webhook.json):
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"backup_id": ..., # Reference ID for the backup
|
||||||
|
"hook_url": "...", # URL of the webhook
|
||||||
|
"settings": [ # Array of settings for this webhook
|
||||||
|
{
|
||||||
|
"recursive": true/false, # Whether webhook applies to subpaths
|
||||||
|
"additional_header": "...", # Additional headers for webhook
|
||||||
|
"enabled": true/false, # Whether webhook is enabled
|
||||||
|
"on_events": ... # Event types that trigger the webhook
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
How to Add New Information to Backup
|
||||||
|
-----------------------------------
|
||||||
|
To add new information to the backup system, follow these steps:
|
||||||
|
|
||||||
|
1. Adding a new field to an existing entity:
|
||||||
|
- For Path entities: Add the field to the path metadata in the traverse() function
|
||||||
|
- For Markdown entities: Add the field to the md_data dictionary in the traverse() function
|
||||||
|
- For Templates: Add the field to the template_dict in export_templates()
|
||||||
|
- For Webhooks: Add the field to the webhook_entry in export_webhooks()
|
||||||
|
|
||||||
|
2. Adding a new entity type:
|
||||||
|
- Create a new export_[entity]() function similar to export_webhooks() or export_templates()
|
||||||
|
- Call this function from get_backup()
|
||||||
|
- Create a corresponding import_[entity]() function for restoration
|
||||||
|
- Call this function from load_backup()
|
||||||
|
|
||||||
|
3. Example: Adding a new "tags" field to markdown:
|
||||||
|
In the traverse() function, modify the md_data creation:
|
||||||
|
|
||||||
|
md_data = {
|
||||||
|
"title": md.title,
|
||||||
|
"content": md.content,
|
||||||
|
"created_at": md.created_at,
|
||||||
|
"order": md.order,
|
||||||
|
"shortcut": md.shortcut,
|
||||||
|
"backup_id": md.id,
|
||||||
|
"tags": md.tags # New field
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Then in process_markdown_file(), handle the new field:
|
||||||
|
|
||||||
|
tags = md_data.get("tags", []) # Get tags with default empty list
|
||||||
|
|
||||||
|
# Later when creating/updating the markdown:
|
||||||
|
if existing_md:
|
||||||
|
existing_md.tags = tags
|
||||||
|
else:
|
||||||
|
new_md = Markdown(
|
||||||
|
# other fields...
|
||||||
|
tags=tags
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
4. Example: Adding a new entity type "Comments":
|
||||||
|
Create export function:
|
||||||
|
|
||||||
|
def export_comments():
|
||||||
|
# Export all comments to comments.json file in the root directory
|
||||||
|
with get_db() as session:
|
||||||
|
comments = session.query(Comment).all()
|
||||||
|
comment_data = []
|
||||||
|
|
||||||
|
for comment in comments:
|
||||||
|
comment_dict = comment.to_dict()
|
||||||
|
# Process and add to comment_data
|
||||||
|
|
||||||
|
with open('comments.json', 'w') as f:
|
||||||
|
json.dump(comment_data, f, default=str, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
Call it from get_backup():
|
||||||
|
|
||||||
|
# After other exports
|
||||||
|
export_comments()
|
||||||
|
|
||||||
|
|
||||||
|
Create import function:
|
||||||
|
|
||||||
|
def import_comments(comments_file, session):
|
||||||
|
# Logic to import comments
|
||||||
|
|
||||||
|
|
||||||
|
Call it from load_backup():
|
||||||
|
|
||||||
|
# After other imports
|
||||||
|
import_comments(os.path.join(root_dir, "comments.json"), session)
|
||||||
|
|
||||||
|
|
||||||
|
Maintaining Backward Compatibility
|
||||||
|
---------------------------------
|
||||||
|
When adding new fields or entities:
|
||||||
|
1. Always use .get() with default values when reading JSON data
|
||||||
|
2. Check if fields exist before accessing them
|
||||||
|
3. Handle both old and new formats in import functions
|
||||||
|
4. Use conditional logic to process data based on available fields
|
||||||
|
5. Keep the basic structure of the backup intact
|
||||||
|
|
||||||
|
For example, in process_markdown_file():
|
||||||
|
|
||||||
|
# Handle both old and new formats
|
||||||
|
if "setting" in md_data:
|
||||||
|
# Process new format
|
||||||
|
else:
|
||||||
|
# Process old format for backward compatibility
|
||||||
|
|
||||||
|
|
||||||
|
ID Handling
|
||||||
|
----------
|
||||||
|
The backup system maintains its own ID references:
|
||||||
|
1. Database IDs are not directly used in the backup
|
||||||
|
2. Each entity gets a backup_id for reference within the backup
|
||||||
|
3. When restoring, new database IDs are generated
|
||||||
|
4. References between entities use lookup by natural keys (e.g., title, URL)
|
||||||
|
"""
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from flask import Blueprint, send_file, jsonify, request
|
from flask import Blueprint, send_file, jsonify, request
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
from api import require_auth
|
from api import require_auth
|
||||||
from db import get_db
|
from db import get_db
|
||||||
from db.models.Markdown import Markdown
|
from db.models.Markdown import Markdown
|
||||||
from db.models.Path import Path
|
from db.models.Path import Path
|
||||||
|
from db.models.MarkdownSetting import MarkdownSetting
|
||||||
|
from db.models.MarkdownTemplateSetting import MarkdownTemplateSetting
|
||||||
|
from db.models.MarkdownPermissionSetting import MarkdownPermissionSetting
|
||||||
|
from db.models.MarkdownTemplate import MarkdownTemplate
|
||||||
|
from db.models.PathSetting import PathSetting
|
||||||
|
from db.models.WebhookSetting import WebhookSetting
|
||||||
|
from db.models.PathTemplate import PathTemplate
|
||||||
|
from db.models.Webhook import Webhook
|
||||||
import threading
|
import threading
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
backup_bp = Blueprint('backup', __name__, url_prefix='/api/backup')
|
backup_bp = Blueprint('backup', __name__, url_prefix='/api/backup')
|
||||||
|
|
||||||
|
def check_and_convert_backup_version(backup_dir):
|
||||||
|
"""
|
||||||
|
Check the backup version and convert it if necessary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backup_dir (str): Path to the backup directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (success, error_response)
|
||||||
|
- success (bool): True if the check and conversion was successful, False otherwise
|
||||||
|
- error_response: None if successful, otherwise a Flask response object with an error message
|
||||||
|
"""
|
||||||
|
from misc.backup_converters import get_backup_version, CURRENT_VERSION, convert_backup
|
||||||
|
backup_version = get_backup_version(backup_dir)
|
||||||
|
|
||||||
|
if backup_version != CURRENT_VERSION:
|
||||||
|
logger.info(f"Converting backup from version {backup_version} to {CURRENT_VERSION}")
|
||||||
|
try:
|
||||||
|
convert_backup(backup_dir, CURRENT_VERSION)
|
||||||
|
return True, None
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Failed to convert backup: {e}")
|
||||||
|
return False, jsonify({"error": f"Failed to convert backup: {e}"}), 400
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
@backup_bp.route('/convert', methods=['POST'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def convert_backup_endpoint():
|
||||||
|
"""
|
||||||
|
Convert an old version backup to the current version format.
|
||||||
|
|
||||||
|
This endpoint accepts an uploaded backup file, converts it to the current version format,
|
||||||
|
and returns the converted backup file as an attachment. The conversion process handles the differences
|
||||||
|
between different backup formats, including the directory structure, file formats, and metadata.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- file: The backup file to convert (multipart/form-data)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The converted backup file as an attachment.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Conversion successful
|
||||||
|
- 400: No file provided or invalid file
|
||||||
|
- 429: Another backup operation is in progress
|
||||||
|
- 500: Conversion failed
|
||||||
|
"""
|
||||||
|
if not backup_lock.acquire(blocking=False):
|
||||||
|
return jsonify({"error": "Another backup operation is in progress. Please try again later."}), 429
|
||||||
|
|
||||||
|
try:
|
||||||
|
if 'file' not in request.files:
|
||||||
|
return jsonify({"error": "No file provided"}), 400
|
||||||
|
|
||||||
|
uploaded_file = request.files['file']
|
||||||
|
|
||||||
|
temp_dir = tempfile.mkdtemp()
|
||||||
|
backup_dir = os.path.join(temp_dir, "backup")
|
||||||
|
os.makedirs(backup_dir)
|
||||||
|
|
||||||
|
zip_path = os.path.join(temp_dir, "backup.zip")
|
||||||
|
uploaded_file.save(zip_path)
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(backup_dir)
|
||||||
|
|
||||||
|
success, error_response = check_and_convert_backup_version(backup_dir)
|
||||||
|
if not success:
|
||||||
|
return error_response
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
|
||||||
|
archive_name = f"converted_backup_{timestamp}"
|
||||||
|
archive_path = shutil.make_archive(
|
||||||
|
base_name=os.path.join(temp_dir, archive_name),
|
||||||
|
format='zip',
|
||||||
|
root_dir=backup_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
shutil.rmtree(backup_dir)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
archive_path,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f"{archive_name}.zip",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to convert backup: {e}")
|
||||||
|
return jsonify({"error": f"Failed to convert backup: {e}"}), 500
|
||||||
|
|
||||||
|
finally:
|
||||||
|
backup_lock.release()
|
||||||
|
|
||||||
|
|
||||||
backup_lock = threading.Lock()
|
backup_lock = threading.Lock()
|
||||||
@backup_bp.route('/', methods=['GET'])
|
@backup_bp.route('/', methods=['GET'])
|
||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
def get_backup():
|
def get_backup():
|
||||||
|
"""
|
||||||
|
Create a backup of the application's data.
|
||||||
|
|
||||||
|
This function creates a backup of the application's data, including:
|
||||||
|
- The tree structure (paths and markdowns)
|
||||||
|
- Templates
|
||||||
|
- Webhooks
|
||||||
|
- Version information
|
||||||
|
|
||||||
|
The backup is returned as a zip file attachment.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A zip file containing the backup data.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Backup created successfully
|
||||||
|
- 500: Failed to create backup
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
|
if os.path.exists('Root'):
|
||||||
|
shutil.rmtree('Root')
|
||||||
|
os.makedirs('Root')
|
||||||
|
os.chdir('Root')
|
||||||
|
|
||||||
|
os.makedirs('tree')
|
||||||
|
os.makedirs('template')
|
||||||
|
|
||||||
|
from misc.backup_converters import CURRENT_VERSION
|
||||||
|
with open('version.json', 'w') as f:
|
||||||
|
json.dump({"version": CURRENT_VERSION}, f, indent=2)
|
||||||
|
|
||||||
|
export_webhooks()
|
||||||
|
export_templates()
|
||||||
|
|
||||||
paths = {}
|
paths = {}
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
pths = session.query(Path).all()
|
pths = session.query(Path).all()
|
||||||
paths = {p.id : p for p in pths}
|
paths = {p.id : p for p in pths}
|
||||||
|
|
||||||
|
os.chdir('tree')
|
||||||
traverse(1, paths)
|
traverse(1, paths)
|
||||||
|
os.chdir('..')
|
||||||
|
|
||||||
|
os.chdir('..')
|
||||||
timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
|
timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
|
||||||
archive = shutil.make_archive(base_name=timestamp, format='zip', root_dir='Root')
|
archive = shutil.make_archive(base_name=timestamp, format='zip', root_dir='Root')
|
||||||
|
|
||||||
@@ -45,32 +380,165 @@ def create_and_cd(path_name):
|
|||||||
def cd_back():
|
def cd_back():
|
||||||
os.chdir('..')
|
os.chdir('..')
|
||||||
|
|
||||||
|
def export_webhooks():
|
||||||
|
with get_db() as session:
|
||||||
|
webhooks = session.query(Webhook).all()
|
||||||
|
webhook_data = []
|
||||||
|
|
||||||
|
for webhook in webhooks:
|
||||||
|
webhook_dict = webhook.to_dict()
|
||||||
|
backup_id = len(webhook_data) + 1
|
||||||
|
|
||||||
|
webhook_settings = session.query(WebhookSetting).filter_by(webhook_id=webhook.id).all()
|
||||||
|
settings_list = []
|
||||||
|
|
||||||
|
for setting in webhook_settings:
|
||||||
|
setting_dict = setting.to_dict()
|
||||||
|
setting_dict.pop('id', None)
|
||||||
|
setting_dict.pop('webhook_id', None)
|
||||||
|
settings_list.append(setting_dict)
|
||||||
|
|
||||||
|
webhook_entry = {
|
||||||
|
'backup_id': backup_id,
|
||||||
|
'hook_url': webhook_dict['hook_url'],
|
||||||
|
'settings': settings_list
|
||||||
|
}
|
||||||
|
webhook_data.append(webhook_entry)
|
||||||
|
|
||||||
|
with open('webhook.json', 'w') as f:
|
||||||
|
json.dump(webhook_data, f, default=str, indent=2)
|
||||||
|
|
||||||
|
def export_templates():
|
||||||
|
with get_db() as session:
|
||||||
|
templates = session.query(MarkdownTemplate).all()
|
||||||
|
|
||||||
|
for template in templates:
|
||||||
|
template_dict = template.to_dict()
|
||||||
|
filename = f"{template_dict['title']}.json"
|
||||||
|
|
||||||
|
template_dict.pop('id', None)
|
||||||
|
|
||||||
|
with open(os.path.join('template', filename), 'w') as f:
|
||||||
|
json.dump(template_dict, f, default=str, indent=2)
|
||||||
|
|
||||||
def traverse(path_id, paths):
|
def traverse(path_id, paths):
|
||||||
current_path = paths[path_id]
|
current_path = paths[path_id]
|
||||||
if path_id == 1:
|
if path_id != 1:
|
||||||
create_and_cd("Root")
|
|
||||||
else:
|
|
||||||
create_and_cd(current_path.name)
|
create_and_cd(current_path.name)
|
||||||
with open(".meta", "w") as meta_file:
|
|
||||||
meta_file.write(f"order: {current_path.order}\n")
|
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
|
path_meta = {
|
||||||
|
"name": current_path.name,
|
||||||
|
"order": current_path.order,
|
||||||
|
"backup_id": path_id
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_path.setting_id:
|
||||||
|
path_setting = session.query(PathSetting).get(current_path.setting_id)
|
||||||
|
if path_setting:
|
||||||
|
if path_setting.webhook_setting_id:
|
||||||
|
webhook_setting = session.query(WebhookSetting).get(path_setting.webhook_setting_id)
|
||||||
|
if webhook_setting:
|
||||||
|
if webhook_setting.webhook_id:
|
||||||
|
webhook = session.query(Webhook).get(webhook_setting.webhook_id)
|
||||||
|
if webhook:
|
||||||
|
path_meta["webhook_ref"] = {
|
||||||
|
"hook_url": webhook.hook_url
|
||||||
|
}
|
||||||
|
|
||||||
|
path_meta["webhook_setting"] = {
|
||||||
|
"recursive": webhook_setting.recursive,
|
||||||
|
"additional_header": webhook_setting.additional_header,
|
||||||
|
"enabled": webhook_setting.enabled,
|
||||||
|
"on_events": webhook_setting.on_events
|
||||||
|
}
|
||||||
|
|
||||||
|
if path_setting.template_setting_id:
|
||||||
|
path_template = session.query(PathTemplate).get(path_setting.template_setting_id)
|
||||||
|
if path_template:
|
||||||
|
path_meta["template_ref"] = {
|
||||||
|
"title": path_template.title
|
||||||
|
}
|
||||||
|
path_meta["template_setting"] = {
|
||||||
|
"template_id": path_setting.template_setting_id
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(".json.meta", "w") as meta_file:
|
||||||
|
json.dump(path_meta, meta_file, default=str, indent=2)
|
||||||
|
|
||||||
mds = session.query(Markdown).filter(Markdown.path_id == path_id).all()
|
mds = session.query(Markdown).filter(Markdown.path_id == path_id).all()
|
||||||
for md in mds:
|
for md in mds:
|
||||||
with open(f"{md.title}.md", "w") as md_file:
|
md_data = {
|
||||||
md_file.write(md.content)
|
"title": md.title,
|
||||||
with open(f"{md.title}.mdmeta", "w") as meta_file:
|
"content": md.content,
|
||||||
meta_file.write(f"created_at: {md.created_at}\n")
|
"created_at": md.created_at,
|
||||||
meta_file.write(f"order: {md.order}\n")
|
"order": md.order,
|
||||||
meta_file.write(f"shortcut: {md.shortcut}\n")
|
"shortcut": md.shortcut,
|
||||||
|
"backup_id": md.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if md.setting_id:
|
||||||
|
md_setting = session.query(MarkdownSetting).get(md.setting_id)
|
||||||
|
if md_setting:
|
||||||
|
settings = {}
|
||||||
|
|
||||||
|
if md_setting.template_setting_id:
|
||||||
|
template_setting = session.query(MarkdownTemplateSetting).get(md_setting.template_setting_id)
|
||||||
|
if template_setting and template_setting.template_id:
|
||||||
|
template = session.query(MarkdownTemplate).get(template_setting.template_id)
|
||||||
|
if template:
|
||||||
|
settings["template_setting"] = {
|
||||||
|
"template_id": template_setting.template_id,
|
||||||
|
"template_ref": {
|
||||||
|
"title": template.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if md_setting.permission_setting_id:
|
||||||
|
permission_setting = session.query(MarkdownPermissionSetting).get(md_setting.permission_setting_id)
|
||||||
|
if permission_setting:
|
||||||
|
settings["permission_setting"] = permission_setting.permission
|
||||||
|
|
||||||
|
if settings:
|
||||||
|
md_data["setting"] = settings
|
||||||
|
|
||||||
|
with open(f"{md.title}.json", "w") as md_file:
|
||||||
|
json.dump(md_data, md_file, default=str, indent=2)
|
||||||
|
|
||||||
children = [c for c in paths.values() if c.parent_id == path_id]
|
children = [c for c in paths.values() if c.parent_id == path_id]
|
||||||
for child in children:
|
for child in children:
|
||||||
traverse(child.id, paths)
|
traverse(child.id, paths)
|
||||||
cd_back()
|
|
||||||
|
if path_id != 1:
|
||||||
|
cd_back()
|
||||||
|
|
||||||
|
|
||||||
@backup_bp.route('/load', methods=['POST'])
|
@backup_bp.route('/load', methods=['POST'])
|
||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
def load_backup():
|
def load_backup():
|
||||||
|
"""
|
||||||
|
Restore data from a backup file.
|
||||||
|
|
||||||
|
This function restores data from a backup file, including:
|
||||||
|
- The tree structure (paths and markdowns)
|
||||||
|
- Templates
|
||||||
|
- Webhooks
|
||||||
|
|
||||||
|
If the backup version does not match the current version, the backup will be
|
||||||
|
automatically converted to the current version before being restored.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- file: The backup file to restore (multipart/form-data)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object with a success message.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Backup restored successfully
|
||||||
|
- 400: No file provided or invalid backup format
|
||||||
|
- 429: Another backup restore is in progress
|
||||||
|
- 500: Failed to restore backup
|
||||||
|
"""
|
||||||
if not backup_lock.acquire(blocking=False):
|
if not backup_lock.acquire(blocking=False):
|
||||||
return jsonify({"error": "Another backup restore is in progress. Please try again later."}), 429
|
return jsonify({"error": "Another backup restore is in progress. Please try again later."}), 429
|
||||||
|
|
||||||
@@ -86,18 +554,32 @@ def load_backup():
|
|||||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
zip_ref.extractall(temp_dir)
|
zip_ref.extractall(temp_dir)
|
||||||
|
|
||||||
root_dir = temp_dir
|
root_dir = os.path.join(temp_dir, "Root")
|
||||||
if not os.path.exists(root_dir):
|
if not os.path.exists(root_dir):
|
||||||
return jsonify({"error": "Invalid backup format"}), 400
|
root_dir = temp_dir
|
||||||
|
|
||||||
|
success, error_response = check_and_convert_backup_version(root_dir)
|
||||||
|
if not success:
|
||||||
|
return error_response
|
||||||
|
|
||||||
|
tree_dir = os.path.join(root_dir, "tree")
|
||||||
|
template_dir = os.path.join(root_dir, "template")
|
||||||
|
|
||||||
|
if not os.path.exists(tree_dir) or not os.path.exists(template_dir):
|
||||||
|
return jsonify({"error": "Invalid backup format: missing tree or template directory"}), 400
|
||||||
|
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
|
import_templates(template_dir, session)
|
||||||
|
|
||||||
|
webhook_mapping = import_webhooks(os.path.join(root_dir, "webhook.json"), session)
|
||||||
|
|
||||||
path_mapping = {}
|
path_mapping = {}
|
||||||
restore_tree(root_dir, None, session, path_mapping)
|
restore_tree(tree_dir, None, session, path_mapping, webhook_mapping)
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
shutil.rmtree(temp_dir)
|
shutil.rmtree(temp_dir)
|
||||||
|
return jsonify({"success": True, "message": "Backup restored and merged successfully"}), 200
|
||||||
return jsonify({"success": True, "message": "Backup restored and merged successfully"})
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load backup: {e}")
|
logger.error(f"Failed to load backup: {e}")
|
||||||
@@ -106,26 +588,264 @@ def load_backup():
|
|||||||
backup_lock.release()
|
backup_lock.release()
|
||||||
|
|
||||||
|
|
||||||
def restore_tree(dir_path, parent_id, session, path_mapping):
|
def import_templates(template_dir, session):
|
||||||
|
template_mapping = {}
|
||||||
|
|
||||||
|
for filename in os.listdir(template_dir):
|
||||||
|
if filename.endswith('.json'):
|
||||||
|
file_path = os.path.join(template_dir, filename)
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
template_data = json.load(f)
|
||||||
|
|
||||||
|
title = template_data.get('title')
|
||||||
|
if not title:
|
||||||
|
title = os.path.splitext(filename)[0]
|
||||||
|
|
||||||
|
existing_template = session.query(MarkdownTemplate).filter_by(title=title).first()
|
||||||
|
if existing_template:
|
||||||
|
template_mapping[title] = existing_template.id
|
||||||
|
else:
|
||||||
|
new_template = MarkdownTemplate(
|
||||||
|
title=title,
|
||||||
|
parameters=template_data.get('parameters'),
|
||||||
|
layout=template_data.get('layout')
|
||||||
|
)
|
||||||
|
session.add(new_template)
|
||||||
|
session.flush()
|
||||||
|
template_mapping[title] = new_template.id
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error importing template {filename}: {e}")
|
||||||
|
|
||||||
|
return template_mapping
|
||||||
|
|
||||||
|
def import_webhooks(webhook_file, session):
|
||||||
|
webhook_mapping = {}
|
||||||
|
|
||||||
|
if not os.path.exists(webhook_file):
|
||||||
|
logger.warning(f"Webhook file not found: {webhook_file}")
|
||||||
|
return webhook_mapping
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(webhook_file, 'r') as f:
|
||||||
|
webhook_data = json.load(f)
|
||||||
|
|
||||||
|
for webhook_entry in webhook_data:
|
||||||
|
backup_id = webhook_entry.get('backup_id')
|
||||||
|
hook_url = webhook_entry.get('hook_url')
|
||||||
|
|
||||||
|
if not hook_url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
existing_webhook = session.query(Webhook).filter_by(hook_url=hook_url).first()
|
||||||
|
if existing_webhook:
|
||||||
|
webhook_id = existing_webhook.id
|
||||||
|
else:
|
||||||
|
new_webhook = Webhook(hook_url=hook_url)
|
||||||
|
session.add(new_webhook)
|
||||||
|
session.flush()
|
||||||
|
webhook_id = new_webhook.id
|
||||||
|
|
||||||
|
webhook_mapping[backup_id] = webhook_id
|
||||||
|
|
||||||
|
settings = webhook_entry.get('settings', [])
|
||||||
|
for setting_data in settings:
|
||||||
|
new_setting = WebhookSetting(
|
||||||
|
webhook_id=webhook_id,
|
||||||
|
recursive=setting_data.get('recursive', False),
|
||||||
|
additional_header=setting_data.get('additional_header'),
|
||||||
|
enabled=setting_data.get('enabled', True),
|
||||||
|
on_events=setting_data.get('on_events', 0)
|
||||||
|
)
|
||||||
|
session.add(new_setting)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error importing webhooks: {e}")
|
||||||
|
|
||||||
|
return webhook_mapping
|
||||||
|
|
||||||
|
def process_markdown_file(file_path, file_name, new_path_id, session):
|
||||||
|
try:
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
md_data = json.load(f)
|
||||||
|
|
||||||
|
md_title = md_data.get("title")
|
||||||
|
if not md_title:
|
||||||
|
md_title = os.path.splitext(file_name)[0]
|
||||||
|
|
||||||
|
content = md_data.get("content", "")
|
||||||
|
created_at_str = md_data.get("created_at")
|
||||||
|
created_at = datetime.now()
|
||||||
|
if created_at_str:
|
||||||
|
try:
|
||||||
|
created_at = datetime.strptime(created_at_str, "%Y-%m-%d %H:%M:%S")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
order = md_data.get("order", "")
|
||||||
|
shortcut = md_data.get("shortcut", "")
|
||||||
|
|
||||||
|
setting_id = None
|
||||||
|
template_setting_id = None
|
||||||
|
permission_setting_id = None
|
||||||
|
|
||||||
|
if "setting" in md_data:
|
||||||
|
settings = md_data.get("setting", {})
|
||||||
|
|
||||||
|
if "template_setting" in settings:
|
||||||
|
template_setting_data = settings["template_setting"]
|
||||||
|
template_title = None
|
||||||
|
|
||||||
|
if isinstance(template_setting_data, dict) and "template_ref" in template_setting_data:
|
||||||
|
template_title = template_setting_data["template_ref"].get("title")
|
||||||
|
|
||||||
|
if template_title:
|
||||||
|
existing_template = session.query(MarkdownTemplate).filter_by(title=template_title).first()
|
||||||
|
if existing_template:
|
||||||
|
template_id = existing_template.id
|
||||||
|
new_template_setting = MarkdownTemplateSetting(template_id=template_id)
|
||||||
|
session.add(new_template_setting)
|
||||||
|
session.flush()
|
||||||
|
template_setting_id = new_template_setting.id
|
||||||
|
|
||||||
|
if "permission_setting" in settings:
|
||||||
|
permission = settings["permission_setting"]
|
||||||
|
if permission:
|
||||||
|
new_permission_setting = MarkdownPermissionSetting(permission=permission)
|
||||||
|
session.add(new_permission_setting)
|
||||||
|
session.flush()
|
||||||
|
permission_setting_id = new_permission_setting.id
|
||||||
|
else:
|
||||||
|
if "template_ref" in md_data:
|
||||||
|
template_title = md_data["template_ref"].get("title")
|
||||||
|
if template_title:
|
||||||
|
existing_template = session.query(MarkdownTemplate).filter_by(title=template_title).first()
|
||||||
|
if existing_template:
|
||||||
|
template_id = existing_template.id
|
||||||
|
if "template_setting" in md_data:
|
||||||
|
pass
|
||||||
|
new_template_setting = MarkdownTemplateSetting(template_id=template_id)
|
||||||
|
session.add(new_template_setting)
|
||||||
|
session.flush()
|
||||||
|
template_setting_id = new_template_setting.id
|
||||||
|
|
||||||
|
if "permission" in md_data:
|
||||||
|
permission = md_data.get("permission")
|
||||||
|
if permission:
|
||||||
|
new_permission_setting = MarkdownPermissionSetting(permission=permission)
|
||||||
|
session.add(new_permission_setting)
|
||||||
|
session.flush()
|
||||||
|
permission_setting_id = new_permission_setting.id
|
||||||
|
|
||||||
|
if template_setting_id or permission_setting_id:
|
||||||
|
md_setting = MarkdownSetting(
|
||||||
|
template_setting_id=template_setting_id,
|
||||||
|
permission_setting_id=permission_setting_id
|
||||||
|
)
|
||||||
|
session.add(md_setting)
|
||||||
|
session.flush()
|
||||||
|
setting_id = md_setting.id
|
||||||
|
|
||||||
|
existing_md = session.query(Markdown).filter_by(path_id=new_path_id, title=md_title).first()
|
||||||
|
|
||||||
|
if existing_md:
|
||||||
|
existing_md.content = content
|
||||||
|
existing_md.created_at = created_at
|
||||||
|
existing_md.order = order
|
||||||
|
existing_md.shortcut = shortcut
|
||||||
|
existing_md.setting_id = setting_id
|
||||||
|
session.commit()
|
||||||
|
else:
|
||||||
|
new_md = Markdown(
|
||||||
|
title=md_title,
|
||||||
|
content=content,
|
||||||
|
path_id=new_path_id,
|
||||||
|
created_at=created_at,
|
||||||
|
order=order,
|
||||||
|
shortcut=shortcut,
|
||||||
|
setting_id=setting_id
|
||||||
|
)
|
||||||
|
session.add(new_md)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing markdown file {file_name}: {e}")
|
||||||
|
|
||||||
|
def restore_tree(dir_path, parent_id, session, path_mapping, webhook_mapping=None):
|
||||||
|
if webhook_mapping is None:
|
||||||
|
webhook_mapping = {}
|
||||||
|
|
||||||
dir_name = os.path.basename(dir_path)
|
dir_name = os.path.basename(dir_path)
|
||||||
|
|
||||||
existing_path = session.query(Path).filter_by(parent_id=parent_id, name=dir_name).first()
|
if dir_name == "Root" or dir_name == "tree":
|
||||||
if parent_id is None:
|
|
||||||
new_path_id = 1
|
new_path_id = 1
|
||||||
elif existing_path:
|
path_mapping[dir_path] = new_path_id
|
||||||
|
|
||||||
|
for item in os.listdir(dir_path):
|
||||||
|
item_path = os.path.join(dir_path, item)
|
||||||
|
if os.path.isdir(item_path):
|
||||||
|
restore_tree(item_path, 1, session, path_mapping, webhook_mapping)
|
||||||
|
elif item.endswith(".json") and not item == ".json.meta":
|
||||||
|
process_markdown_file(item_path, item, new_path_id, session)
|
||||||
|
return
|
||||||
|
existing_path = session.query(Path).filter_by(parent_id=parent_id, name=dir_name).first()
|
||||||
|
if existing_path:
|
||||||
new_path_id = existing_path.id
|
new_path_id = existing_path.id
|
||||||
else:
|
else:
|
||||||
order = ''
|
order = ''
|
||||||
meta_file_path = os.path.join(dir_path, ".meta")
|
setting_id = None
|
||||||
if os.path.exists(meta_file_path):
|
|
||||||
with open(meta_file_path, "r") as meta_file:
|
|
||||||
for line in meta_file:
|
|
||||||
key, value = line.strip().split(": ", 1)
|
|
||||||
if key == "order":
|
|
||||||
order = value
|
|
||||||
|
|
||||||
new_path = Path(name=dir_name, parent_id=parent_id, order=order)
|
meta_file_path = os.path.join(dir_path, ".json.meta")
|
||||||
|
if os.path.exists(meta_file_path):
|
||||||
|
try:
|
||||||
|
with open(meta_file_path, "r") as meta_file:
|
||||||
|
path_meta = json.load(meta_file)
|
||||||
|
order = path_meta.get("order", '')
|
||||||
|
|
||||||
|
webhook_setting_id = None
|
||||||
|
if "webhook_ref" in path_meta and "webhook_setting" in path_meta:
|
||||||
|
hook_url = path_meta["webhook_ref"].get("hook_url")
|
||||||
|
|
||||||
|
webhook_id = None
|
||||||
|
existing_webhook = session.query(Webhook).filter_by(hook_url=hook_url).first()
|
||||||
|
if existing_webhook:
|
||||||
|
webhook_id = existing_webhook.id
|
||||||
|
else:
|
||||||
|
new_webhook = Webhook(hook_url=hook_url)
|
||||||
|
session.add(new_webhook)
|
||||||
|
session.flush()
|
||||||
|
webhook_id = new_webhook.id
|
||||||
|
|
||||||
|
if webhook_id:
|
||||||
|
webhook_setting_data = path_meta["webhook_setting"]
|
||||||
|
new_webhook_setting = WebhookSetting(
|
||||||
|
webhook_id=webhook_id,
|
||||||
|
recursive=webhook_setting_data.get("recursive", False),
|
||||||
|
additional_header=webhook_setting_data.get("additional_header"),
|
||||||
|
enabled=webhook_setting_data.get("enabled", True),
|
||||||
|
on_events=webhook_setting_data.get("on_events", 0)
|
||||||
|
)
|
||||||
|
session.add(new_webhook_setting)
|
||||||
|
session.flush()
|
||||||
|
webhook_setting_id = new_webhook_setting.id
|
||||||
|
|
||||||
|
template_setting_id = None
|
||||||
|
if "template_ref" in path_meta:
|
||||||
|
template_title = path_meta["template_ref"].get("title")
|
||||||
|
|
||||||
|
existing_template = session.query(PathTemplate).filter_by(title=template_title).first()
|
||||||
|
if existing_template:
|
||||||
|
template_setting_id = existing_template.id
|
||||||
|
|
||||||
|
if webhook_setting_id or template_setting_id:
|
||||||
|
path_setting = PathSetting(
|
||||||
|
webhook_setting_id=webhook_setting_id,
|
||||||
|
template_setting_id=template_setting_id
|
||||||
|
)
|
||||||
|
session.add(path_setting)
|
||||||
|
session.flush()
|
||||||
|
setting_id = path_setting.id
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing path metadata: {e}")
|
||||||
|
|
||||||
|
new_path = Path(name=dir_name, parent_id=parent_id, order=order, setting_id=setting_id)
|
||||||
session.add(new_path)
|
session.add(new_path)
|
||||||
session.flush()
|
session.flush()
|
||||||
new_path_id = new_path.id
|
new_path_id = new_path.id
|
||||||
@@ -134,44 +854,10 @@ def restore_tree(dir_path, parent_id, session, path_mapping):
|
|||||||
|
|
||||||
for file in os.listdir(dir_path):
|
for file in os.listdir(dir_path):
|
||||||
file_path = os.path.join(dir_path, file)
|
file_path = os.path.join(dir_path, file)
|
||||||
if file.endswith(".md"):
|
if file.endswith(".json") and not file == ".json.meta":
|
||||||
md_title = file[:-3]
|
process_markdown_file(file_path, file, new_path_id, session)
|
||||||
|
|
||||||
mdmeta_path = file_path + "meta"
|
|
||||||
created_at = datetime.now()
|
|
||||||
order = ''
|
|
||||||
shortcut = ""
|
|
||||||
|
|
||||||
if os.path.exists(mdmeta_path):
|
|
||||||
with open(mdmeta_path, "r") as meta_file:
|
|
||||||
for line in meta_file:
|
|
||||||
key, value = line.strip().split(": ", 1)
|
|
||||||
if key == "created_at":
|
|
||||||
created_at = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
|
||||||
elif key == "order":
|
|
||||||
order = value
|
|
||||||
elif key == "shortcut":
|
|
||||||
shortcut = value
|
|
||||||
|
|
||||||
with open(file_path, "r", encoding="utf-8") as md_file:
|
|
||||||
content = md_file.read()
|
|
||||||
|
|
||||||
unique_title = get_unique_markdown_title(session, md_title, new_path_id)
|
|
||||||
|
|
||||||
new_md = Markdown(title=unique_title, content=content, path_id=new_path_id,
|
|
||||||
created_at=created_at, order=order, shortcut=shortcut)
|
|
||||||
session.add(new_md)
|
|
||||||
|
|
||||||
|
|
||||||
for item in os.listdir(dir_path):
|
for item in os.listdir(dir_path):
|
||||||
item_path = os.path.join(dir_path, item)
|
item_path = os.path.join(dir_path, item)
|
||||||
if os.path.isdir(item_path):
|
if os.path.isdir(item_path):
|
||||||
restore_tree(item_path, new_path_id, session, path_mapping)
|
restore_tree(item_path, new_path_id, session, path_mapping, webhook_mapping)
|
||||||
|
|
||||||
|
|
||||||
def get_unique_markdown_title(session, title, path_id):
|
|
||||||
existing_titles = {md.title for md in session.query(Markdown.title).filter_by(path_id=path_id).all()}
|
|
||||||
unique_title = title
|
|
||||||
while unique_title in existing_titles:
|
|
||||||
unique_title += ".bp"
|
|
||||||
return unique_title
|
|
||||||
|
|||||||
@@ -12,17 +12,48 @@ def is_valid_rate_limit(limit):
|
|||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
@etag_response
|
@etag_response
|
||||||
def limits():
|
def limits():
|
||||||
|
"""
|
||||||
|
Get all rate limits.
|
||||||
|
|
||||||
|
This endpoint retrieves a list of all rate limits configured in the system.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing all rate limits, with keys in the format "endpoint : method".
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
return jsonify(rate_limits), 200
|
return jsonify(rate_limits), 200
|
||||||
|
|
||||||
@config_bp.route('/limits', methods=['PUT'])
|
@config_bp.route('/limits', methods=['PUT'])
|
||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
def update_limits():
|
def update_limits():
|
||||||
|
"""
|
||||||
|
Update a rate limit.
|
||||||
|
|
||||||
|
This endpoint updates the rate limit for a specific endpoint and method.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- endpoint (str): The endpoint path to update
|
||||||
|
- method (str): The HTTP method to update
|
||||||
|
- new_limit (str): The new rate limit value (format: "number per second/minute/hour/day")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object with a success message.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 400: Bad request (missing required fields or invalid rate limit format)
|
||||||
|
- 404: Endpoint not found
|
||||||
|
"""
|
||||||
data = request.json
|
data = request.json
|
||||||
if not data or 'endpoint' not in data or 'method' not in data or 'new_limit' not in data:
|
if not data or 'endpoint' not in data or 'method' not in data or 'new_limit' not in data:
|
||||||
return jsonify({'error': 'Bad request'}), 400
|
return jsonify({'error': 'Bad request'}), 400
|
||||||
key = f"{data['endpoint']} : {data['method']}"
|
key = f"{data['endpoint']} : {data['method']}"
|
||||||
if key not in rate_limits:
|
if key not in rate_limits:
|
||||||
return jsonify({'error': 'endpoint not fount'}), 404
|
return jsonify({'error': 'endpoint not found'}), 404
|
||||||
if is_valid_rate_limit(data['new_limit']):
|
if is_valid_rate_limit(data['new_limit']):
|
||||||
rate_limits[key] = data['new_limit']
|
rate_limits[key] = data['new_limit']
|
||||||
return jsonify({"message": "updated"}), 200
|
return jsonify({"message": "updated"}), 200
|
||||||
|
|||||||
284
api/markdown.py
284
api/markdown.py
@@ -1,12 +1,15 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from api import limiter
|
from api import limiter
|
||||||
from api import require_auth, etag_response
|
from api import require_auth, etag_response, verify_token, is_user_admin
|
||||||
from contexts.RequestContext import RequestContext
|
from contexts.RequestContext import RequestContext
|
||||||
from db import get_db
|
from db import get_db
|
||||||
from db.models.Markdown import Markdown
|
from db.models.Markdown import Markdown
|
||||||
|
from db.models.MarkdownSetting import MarkdownSetting
|
||||||
|
from db.models.MarkdownPermissionSetting import MarkdownPermissionSetting
|
||||||
from events import markdown_created, markdown_updated, markdown_deleted
|
from events import markdown_created, markdown_updated, markdown_deleted
|
||||||
import api
|
import api
|
||||||
|
import env_provider
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -16,6 +19,17 @@ markdown_bp = Blueprint('markdown', __name__, url_prefix='/api/markdown')
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@etag_response
|
@etag_response
|
||||||
def get_markdowns():
|
def get_markdowns():
|
||||||
|
"""
|
||||||
|
Get all markdown documents.
|
||||||
|
|
||||||
|
This endpoint retrieves a list of all markdown documents in the system.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON array containing all markdown documents.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
mds = session.query(Markdown).all()
|
mds = session.query(Markdown).all()
|
||||||
return jsonify([md.to_dict() for md in mds]), 200
|
return jsonify([md.to_dict() for md in mds]), 200
|
||||||
@@ -24,6 +38,19 @@ def get_markdowns():
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@etag_response
|
@etag_response
|
||||||
def get_home():
|
def get_home():
|
||||||
|
"""
|
||||||
|
Get the home markdown document.
|
||||||
|
|
||||||
|
This endpoint retrieves the index markdown document from the root path (path_id=1).
|
||||||
|
This is typically the main landing page content.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the home markdown document.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
- 204: No content (home markdown not found)
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
markdown = session.query(Markdown).filter(Markdown.path_id == 1, Markdown.title == "index").first()
|
markdown = session.query(Markdown).filter(Markdown.path_id == 1, Markdown.title == "index").first()
|
||||||
if markdown is None:
|
if markdown is None:
|
||||||
@@ -34,6 +61,20 @@ def get_home():
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@etag_response
|
@etag_response
|
||||||
def get_markdowns_by_path(path_id):
|
def get_markdowns_by_path(path_id):
|
||||||
|
"""
|
||||||
|
Get all markdown documents in a specific path.
|
||||||
|
|
||||||
|
This endpoint retrieves a list of all markdown documents that belong to the specified path.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- path_id (int): The ID of the path to get markdowns from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON array containing all markdown documents in the specified path.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
markdowns = session.query(Markdown).filter(Markdown.path_id == path_id).all()
|
markdowns = session.query(Markdown).filter(Markdown.path_id == path_id).all()
|
||||||
return jsonify([md.to_dict() for md in markdowns]), 200
|
return jsonify([md.to_dict() for md in markdowns]), 200
|
||||||
@@ -42,6 +83,22 @@ def get_markdowns_by_path(path_id):
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@etag_response
|
@etag_response
|
||||||
def get_index(path_id):
|
def get_index(path_id):
|
||||||
|
"""
|
||||||
|
Get the index markdown document for a specific path.
|
||||||
|
|
||||||
|
This endpoint retrieves the index markdown document from the specified path.
|
||||||
|
The index document typically serves as the main content or landing page for a path.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- path_id (int): The ID of the path to get the index markdown from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the index markdown document.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
- 204: No content (index markdown not found)
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
markdown = session.query(Markdown).filter(Markdown.path_id == path_id).filter(Markdown.title == "index").first()
|
markdown = session.query(Markdown).filter(Markdown.path_id == path_id).filter(Markdown.title == "index").first()
|
||||||
if markdown is None:
|
if markdown is None:
|
||||||
@@ -54,24 +111,77 @@ def get_index(path_id):
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@etag_response
|
@etag_response
|
||||||
def get_markdown(markdown_id):
|
def get_markdown(markdown_id):
|
||||||
|
"""
|
||||||
|
Get a specific markdown document by ID.
|
||||||
|
|
||||||
|
This endpoint retrieves a markdown document by its ID. It includes permission checks
|
||||||
|
based on the user's role and the markdown's permission settings.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- markdown_id (int): The ID of the markdown document to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the markdown document.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
- 203: Permission denied (for protected markdowns when user is not an admin)
|
||||||
|
- 403: Permission denied (for private markdowns when user is not an admin)
|
||||||
|
- 404: Markdown not found
|
||||||
|
"""
|
||||||
|
is_admin = is_user_admin()
|
||||||
|
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
markdown = session.query(Markdown).get(markdown_id)
|
markdown = session.query(Markdown).get(markdown_id)
|
||||||
if markdown is None:
|
if markdown is None:
|
||||||
return jsonify({"error": "file not found"}), 404
|
return jsonify({"error": "file not found"}), 404
|
||||||
|
|
||||||
|
if not is_admin and markdown.setting_id is not None:
|
||||||
|
setting = session.query(MarkdownSetting).get(markdown.setting_id)
|
||||||
|
if setting and setting.permission_setting_id:
|
||||||
|
permission_setting = session.query(MarkdownPermissionSetting).get(setting.permission_setting_id)
|
||||||
|
if permission_setting:
|
||||||
|
if permission_setting.permission == 'private':
|
||||||
|
return jsonify({"msg": "permission denied"}), 403
|
||||||
|
elif permission_setting.permission == 'protected':
|
||||||
|
return jsonify({"msg": "permission denied"}), 203
|
||||||
|
|
||||||
return jsonify(markdown.to_dict()), 200
|
return jsonify(markdown.to_dict()), 200
|
||||||
|
|
||||||
@markdown_bp.route('/', methods=['POST'])
|
@markdown_bp.route('/', methods=['POST'])
|
||||||
@require_auth(roles=['admin', 'creator'])
|
@require_auth(roles=['admin', 'creator'])
|
||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
def create_markdown():
|
def create_markdown():
|
||||||
|
"""
|
||||||
|
Create a new markdown document.
|
||||||
|
|
||||||
|
This endpoint creates a new markdown document with the provided data.
|
||||||
|
It requires authentication with either 'admin' or 'creator' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- title (str): The title of the markdown document
|
||||||
|
- content (str): The content of the markdown document
|
||||||
|
- path_id (int): The ID of the path where the markdown will be created
|
||||||
|
- shortcut (str, optional): A shortcut identifier for the markdown
|
||||||
|
- setting_id (int, optional): The ID of the markdown settings
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the created markdown document.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 201: Created successfully
|
||||||
|
- 400: Bad request (missing required fields or duplicate shortcut)
|
||||||
|
- 500: Server error
|
||||||
|
"""
|
||||||
data = request.json
|
data = request.json
|
||||||
title = data.get('title')
|
title = data.get('title')
|
||||||
content = data.get('content')
|
content = data.get('content')
|
||||||
path_id = data.get('path_id')
|
path_id = data.get('path_id')
|
||||||
shortcut = data.get('shortcut', "")
|
shortcut = data.get('shortcut', "")
|
||||||
|
setting_id = data.get('setting_id', None)
|
||||||
if not title or not content:
|
if not title or not content:
|
||||||
return jsonify({"error": "missing required fields"}), 400
|
return jsonify({"error": "missing required fields"}), 400
|
||||||
new_markdown = Markdown(title=title, content=content, path_id=path_id, shortcut=shortcut)
|
new_markdown = Markdown(title=title, content=content, path_id=path_id, shortcut=shortcut, setting_id=setting_id)
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
try:
|
try:
|
||||||
if shortcut != "":
|
if shortcut != "":
|
||||||
@@ -92,6 +202,31 @@ def create_markdown():
|
|||||||
@require_auth(roles=['admin', 'creator'])
|
@require_auth(roles=['admin', 'creator'])
|
||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
def update_markdown(markdown_id):
|
def update_markdown(markdown_id):
|
||||||
|
"""
|
||||||
|
Update a markdown document.
|
||||||
|
|
||||||
|
This endpoint updates an existing markdown document with the provided data.
|
||||||
|
It requires authentication with either 'admin' or 'creator' role.
|
||||||
|
|
||||||
|
- PUT: Replaces the entire markdown document
|
||||||
|
- PATCH: Updates only the specified fields
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- markdown_id (int): The ID of the markdown document to update
|
||||||
|
- title (str, optional for PATCH): The new title for the markdown
|
||||||
|
- content (str, optional for PATCH): The new content for the markdown
|
||||||
|
- path_id (int, optional for PATCH): The new path ID for the markdown
|
||||||
|
- shortcut (str, optional): A shortcut identifier for the markdown
|
||||||
|
- setting_id (int, optional): The ID of the markdown settings
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated markdown document.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 400: Bad request (duplicate shortcut)
|
||||||
|
- 404: Markdown not found
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
markdown = session.query(Markdown).get(markdown_id)
|
markdown = session.query(Markdown).get(markdown_id)
|
||||||
if markdown is None:
|
if markdown is None:
|
||||||
@@ -110,6 +245,7 @@ def update_markdown(markdown_id):
|
|||||||
markdown.content = data.get('content')
|
markdown.content = data.get('content')
|
||||||
markdown.path_id = data.get('path_id')
|
markdown.path_id = data.get('path_id')
|
||||||
markdown.shortcut = data.get('shortcut', '')
|
markdown.shortcut = data.get('shortcut', '')
|
||||||
|
markdown.setting_id = data.get('setting_id', None)
|
||||||
elif request.method == "PATCH":
|
elif request.method == "PATCH":
|
||||||
if 'title' in data:
|
if 'title' in data:
|
||||||
markdown.title = data.get('title')
|
markdown.title = data.get('title')
|
||||||
@@ -119,6 +255,8 @@ def update_markdown(markdown_id):
|
|||||||
markdown.path_id = data.get('path_id')
|
markdown.path_id = data.get('path_id')
|
||||||
if 'shortcut' in data:
|
if 'shortcut' in data:
|
||||||
markdown.shortcut = data.get('shortcut')
|
markdown.shortcut = data.get('shortcut')
|
||||||
|
if 'setting_id' in data:
|
||||||
|
markdown.setting_id = data.get('setting_id')
|
||||||
session.commit()
|
session.commit()
|
||||||
markdown_updated.send(None, payload=markdown.to_dict())
|
markdown_updated.send(None, payload=markdown.to_dict())
|
||||||
return jsonify(markdown.to_dict()), 200
|
return jsonify(markdown.to_dict()), 200
|
||||||
@@ -127,23 +265,100 @@ def update_markdown(markdown_id):
|
|||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
def delete_markdown(markdown_id):
|
def delete_markdown(markdown_id):
|
||||||
|
"""
|
||||||
|
Delete a markdown document.
|
||||||
|
|
||||||
|
This endpoint deletes an existing markdown document and cascades the deletion
|
||||||
|
to related settings to avoid foreign key conflicts.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- markdown_id (int): The ID of the markdown document to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the deleted markdown document.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Deleted successfully
|
||||||
|
- 404: Markdown not found
|
||||||
|
- 500: Server error during cascade deletion
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
markdown = session.query(Markdown).get(markdown_id)
|
try:
|
||||||
if markdown is None:
|
markdown = session.get(Markdown, markdown_id)
|
||||||
logger.error(f"failed to delete markdown: {markdown_id}")
|
if markdown is None:
|
||||||
|
logger.error(f"failed to delete markdown: {markdown_id}")
|
||||||
|
errno = RequestContext.get_error_id()
|
||||||
|
return jsonify({"error": f"file not found - {errno}"}), 404
|
||||||
|
|
||||||
|
md = markdown.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
if markdown.setting_id:
|
||||||
|
markdown_setting = session.query(MarkdownSetting).get(markdown.setting_id)
|
||||||
|
if markdown_setting:
|
||||||
|
template_setting_id = markdown_setting.template_setting_id
|
||||||
|
permission_setting_id = markdown_setting.permission_setting_id
|
||||||
|
|
||||||
|
markdown_setting.template_setting_id = None
|
||||||
|
markdown_setting.permission_setting_id = None
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
if template_setting_id:
|
||||||
|
from db.models.MarkdownTemplateSetting import MarkdownTemplateSetting
|
||||||
|
template_setting = session.query(MarkdownTemplateSetting).get(template_setting_id)
|
||||||
|
if template_setting:
|
||||||
|
session.delete(template_setting)
|
||||||
|
|
||||||
|
if permission_setting_id:
|
||||||
|
permission_setting = session.query(MarkdownPermissionSetting).get(permission_setting_id)
|
||||||
|
if permission_setting:
|
||||||
|
session.delete(permission_setting)
|
||||||
|
|
||||||
|
session.delete(markdown_setting)
|
||||||
|
|
||||||
|
# Send webhook event before committing the transaction
|
||||||
|
# This ensures webhook handlers can still access related data
|
||||||
|
markdown_deleted.send(None, payload=md)
|
||||||
|
|
||||||
|
session.delete(markdown)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
logger.info(f"Successfully deleted markdown {markdown_id} with cascade deletion")
|
||||||
|
return jsonify(md), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
logger.error(f"Failed to delete markdown {markdown_id}: {e}")
|
||||||
|
logger.error(f"Exception type: {type(e).__name__}")
|
||||||
|
logger.error(f"Full traceback:\n{traceback.format_exc()}")
|
||||||
errno = RequestContext.get_error_id()
|
errno = RequestContext.get_error_id()
|
||||||
return jsonify({"error": f"file not found - {errno}"}), 404
|
session.rollback()
|
||||||
md = markdown.to_dict()
|
return jsonify({"error": f"delete failed - {errno}"}), 500
|
||||||
session.delete(markdown)
|
|
||||||
session.commit()
|
|
||||||
markdown_deleted.send(None, payload=md)
|
|
||||||
return jsonify({"message": "deleted"}), 200
|
|
||||||
|
|
||||||
|
|
||||||
@markdown_bp.route('/move_forward/<int:markdown_id>', methods=['PATCH'])
|
@markdown_bp.route('/move_forward/<int:markdown_id>', methods=['PATCH'])
|
||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
def move_forward(markdown_id):
|
def move_forward(markdown_id):
|
||||||
|
"""
|
||||||
|
Move a markdown document forward in display order.
|
||||||
|
|
||||||
|
This endpoint moves a markdown document one position forward in the display order by swapping its order value
|
||||||
|
with the previous markdown in the same path. This affects how markdowns are displayed in the UI.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- markdown_id (int): The ID of the markdown document to move forward
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated markdown document.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Moved successfully
|
||||||
|
- 400: Bad request (already at the first position)
|
||||||
|
- 404: Markdown not found
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
markdown = session.query(Markdown).get(markdown_id)
|
markdown = session.query(Markdown).get(markdown_id)
|
||||||
if not markdown:
|
if not markdown:
|
||||||
@@ -163,8 +378,26 @@ def move_forward(markdown_id):
|
|||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
def move_backward(markdown_id):
|
def move_backward(markdown_id):
|
||||||
|
"""
|
||||||
|
Move a markdown document backward in display order.
|
||||||
|
|
||||||
|
This endpoint moves a markdown document one position backward in the display order by swapping its order value
|
||||||
|
with the next markdown in the same path. This affects how markdowns are displayed in the UI.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- markdown_id (int): The ID of the markdown document to move backward
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated markdown document.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Moved successfully
|
||||||
|
- 400: Bad request (already at the last position)
|
||||||
|
- 404: Markdown not found
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
markdown = session.query(Markdown).get(markdown_id)
|
markdown = session.get(Markdown, markdown_id)
|
||||||
if not markdown:
|
if not markdown:
|
||||||
return jsonify({"error": "file not found"}), 404
|
return jsonify({"error": "file not found"}), 404
|
||||||
siblings = session.query(Markdown).filter(Markdown.path_id == markdown.path_id).order_by(Markdown.order).all()
|
siblings = session.query(Markdown).filter(Markdown.path_id == markdown.path_id).order_by(Markdown.order).all()
|
||||||
@@ -181,6 +414,21 @@ def move_backward(markdown_id):
|
|||||||
@markdown_bp.route('/search/<string:keyword>', methods=['GET'])
|
@markdown_bp.route('/search/<string:keyword>', methods=['GET'])
|
||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
def search_markdowns(keyword):
|
def search_markdowns(keyword):
|
||||||
|
"""
|
||||||
|
Search for markdown documents.
|
||||||
|
|
||||||
|
This endpoint searches for markdown documents that contain the specified keyword
|
||||||
|
in either their title or content.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- keyword (str): The search term to look for in markdown titles and content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON array containing all markdown documents that match the search criteria.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
res = session.query(Markdown).filter(
|
res = session.query(Markdown).filter(
|
||||||
or_(Markdown.title.like(keyword), Markdown.content.like(keyword))
|
or_(Markdown.title.like(keyword), Markdown.content.like(keyword))
|
||||||
@@ -190,6 +438,18 @@ def search_markdowns(keyword):
|
|||||||
@markdown_bp.route('/links', methods=['GET'])
|
@markdown_bp.route('/links', methods=['GET'])
|
||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
def get_links():
|
def get_links():
|
||||||
|
"""
|
||||||
|
Get all markdown shortcut links.
|
||||||
|
|
||||||
|
This endpoint retrieves a list of all markdown documents that have a shortcut defined,
|
||||||
|
formatted as links in the format "[shortcut]: id".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON array containing formatted links for all markdown documents with shortcuts.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
mds = [md.to_dict() for md in session.query(Markdown).filter(Markdown.shortcut != "").all()]
|
mds = [md.to_dict() for md in session.query(Markdown).filter(Markdown.shortcut != "").all()]
|
||||||
links = [f"[{md['shortcut']}]: {md['id']}" for md in mds]
|
links = [f"[{md['shortcut']}]: {md['id']}" for md in mds]
|
||||||
|
|||||||
155
api/path.py
155
api/path.py
@@ -18,6 +18,17 @@ path_bp = Blueprint('path', __name__, url_prefix='/api/path')
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@etag_response
|
@etag_response
|
||||||
def get_root_paths():
|
def get_root_paths():
|
||||||
|
"""
|
||||||
|
Get all paths under the root path.
|
||||||
|
|
||||||
|
This endpoint retrieves a list of all paths that are direct children of the root path (parent_id=1).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON array containing all root paths.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
paths = session.query(Path).filter(Path.parent_id == 1)
|
paths = session.query(Path).filter(Path.parent_id == 1)
|
||||||
return jsonify([pth.to_dict() for pth in paths]), 200
|
return jsonify([pth.to_dict() for pth in paths]), 200
|
||||||
@@ -26,6 +37,21 @@ def get_root_paths():
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@etag_response
|
@etag_response
|
||||||
def get_path(path_id):
|
def get_path(path_id):
|
||||||
|
"""
|
||||||
|
Get a specific path by ID.
|
||||||
|
|
||||||
|
This endpoint retrieves a path by its ID.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- path_id (int): The ID of the path to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the path.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
- 404: Path not found
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
path = session.query(Path).get(path_id)
|
path = session.query(Path).get(path_id)
|
||||||
if path is None:
|
if path is None:
|
||||||
@@ -36,6 +62,20 @@ def get_path(path_id):
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@etag_response
|
@etag_response
|
||||||
def get_path_by_parent(parent_id):
|
def get_path_by_parent(parent_id):
|
||||||
|
"""
|
||||||
|
Get all paths under a specific parent path.
|
||||||
|
|
||||||
|
This endpoint retrieves a list of all paths that are direct children of the specified parent path.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- parent_id (int): The ID of the parent path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON array containing all child paths of the specified parent.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
paths = session.query(Path).filter(Path.parent_id == parent_id).all()
|
paths = session.query(Path).filter(Path.parent_id == parent_id).all()
|
||||||
return jsonify([pth.to_dict() for pth in paths]), 200
|
return jsonify([pth.to_dict() for pth in paths]), 200
|
||||||
@@ -44,6 +84,25 @@ def get_path_by_parent(parent_id):
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@require_auth(roles=['admin', 'creator'])
|
@require_auth(roles=['admin', 'creator'])
|
||||||
def create_path():
|
def create_path():
|
||||||
|
"""
|
||||||
|
Create a new path.
|
||||||
|
|
||||||
|
This endpoint creates a new path with the provided name and parent ID.
|
||||||
|
It requires authentication with either 'admin' or 'creator' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- name (str): The name of the path
|
||||||
|
- parent_id (int): The ID of the parent path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the created path.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 201: Created successfully
|
||||||
|
- 400: Bad request (missing required fields)
|
||||||
|
- 404: Parent path not found
|
||||||
|
- 409: Conflict (path already exists under the parent)
|
||||||
|
"""
|
||||||
data = request.json
|
data = request.json
|
||||||
if not data or 'name' not in data or 'parent_id' not in data:
|
if not data or 'name' not in data or 'parent_id' not in data:
|
||||||
return jsonify({"error": "bad request"}), 400
|
return jsonify({"error": "bad request"}), 400
|
||||||
@@ -62,6 +121,26 @@ def create_path():
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
def update_path(path_id):
|
def update_path(path_id):
|
||||||
|
"""
|
||||||
|
Update a path.
|
||||||
|
|
||||||
|
This endpoint updates an existing path with the provided name and parent ID.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- path_id (int): The ID of the path to update
|
||||||
|
- name (str): The new name for the path
|
||||||
|
- parent_id (int): The new parent ID for the path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated path.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 400: Bad request (missing required fields)
|
||||||
|
- 404: Path not found
|
||||||
|
- 409: Conflict (path already exists under the parent)
|
||||||
|
"""
|
||||||
data = request.json
|
data = request.json
|
||||||
if not data or 'name' not in data or 'parent_id' not in data:
|
if not data or 'name' not in data or 'parent_id' not in data:
|
||||||
return jsonify({"error": "bad request"}), 400
|
return jsonify({"error": "bad request"}), 400
|
||||||
@@ -81,6 +160,28 @@ def update_path(path_id):
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
def patch_path(path_id):
|
def patch_path(path_id):
|
||||||
|
"""
|
||||||
|
Partially update a path.
|
||||||
|
|
||||||
|
This endpoint partially updates an existing path with the provided data.
|
||||||
|
Unlike the PUT method, this endpoint only updates the fields that are provided in the request.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- path_id (int): The ID of the path to update
|
||||||
|
- name (str, optional): The new name for the path
|
||||||
|
- parent_id (int, optional): The new parent ID for the path
|
||||||
|
- setting_id (int, optional): The new setting ID for the path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated path.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 400: Bad request (empty data)
|
||||||
|
- 404: Path not found
|
||||||
|
- 409: Conflict (path already exists under the parent)
|
||||||
|
"""
|
||||||
data = request.json
|
data = request.json
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({"error": "bad request"}), 400
|
return jsonify({"error": "bad request"}), 400
|
||||||
@@ -106,6 +207,23 @@ def patch_path(path_id):
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
def delete_path(path_id):
|
def delete_path(path_id):
|
||||||
|
"""
|
||||||
|
Delete a path.
|
||||||
|
|
||||||
|
This endpoint deletes an existing path. The path must be empty (no child paths or markdowns)
|
||||||
|
to be deleted. It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- path_id (int): The ID of the path to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object with a success message.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Deleted successfully
|
||||||
|
- 404: Path not found
|
||||||
|
- 409: Conflict (path contains child paths or markdowns)
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
path = session.query(Path).get(path_id)
|
path = session.query(Path).get(path_id)
|
||||||
if not path:
|
if not path:
|
||||||
@@ -125,6 +243,24 @@ def delete_path(path_id):
|
|||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
def move_forward(path_id):
|
def move_forward(path_id):
|
||||||
|
"""
|
||||||
|
Move a path forward in display order.
|
||||||
|
|
||||||
|
This endpoint moves a path one position forward in the display order by swapping its order value
|
||||||
|
with the previous path in the same parent. This affects how paths are displayed in the UI.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- path_id (int): The ID of the path to move forward
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated path.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Moved successfully
|
||||||
|
- 400: Bad request (already at the first position)
|
||||||
|
- 404: Path not found
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
path = session.query(Path).get(path_id)
|
path = session.query(Path).get(path_id)
|
||||||
if not path:
|
if not path:
|
||||||
@@ -144,6 +280,24 @@ def move_forward(path_id):
|
|||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
def move_backward(path_id):
|
def move_backward(path_id):
|
||||||
|
"""
|
||||||
|
Move a path backward in display order.
|
||||||
|
|
||||||
|
This endpoint moves a path one position backward in the display order by swapping its order value
|
||||||
|
with the next path in the same parent. This affects how paths are displayed in the UI.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- path_id (int): The ID of the path to move backward
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated path.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Moved successfully
|
||||||
|
- 400: Bad request (already at the last position)
|
||||||
|
- 404: Path not found
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
path = session.query(Path).get(path_id)
|
path = session.query(Path).get(path_id)
|
||||||
if not path:
|
if not path:
|
||||||
@@ -158,4 +312,3 @@ def move_backward(path_id):
|
|||||||
session.commit()
|
session.commit()
|
||||||
path_updated.send(None, payload=path.to_dict())
|
path_updated.send(None, payload=path.to_dict())
|
||||||
return jsonify(path.to_dict()), 200
|
return jsonify(path.to_dict()), 200
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#api/resource.py
|
|
||||||
import api
|
import api
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
from contexts.RequestContext import RequestContext
|
from contexts.RequestContext import RequestContext
|
||||||
|
|||||||
122
api/setting.py
122
api/setting.py
@@ -1,122 +0,0 @@
|
|||||||
from flask import Blueprint, jsonify, request
|
|
||||||
from api import limiter, etag_response, require_auth
|
|
||||||
from db import get_db
|
|
||||||
from db.models.MarkdownSetting import MarkdownSetting
|
|
||||||
from db.models.PathSetting import PathSetting
|
|
||||||
import api
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
setting_bp = Blueprint('setting', __name__, url_prefix='/api/setting')
|
|
||||||
|
|
||||||
@setting_bp.route('/path/<int:setting_id>', methods=['GET'])
|
|
||||||
@limiter.limit(api.get_rate_limit)
|
|
||||||
@etag_response
|
|
||||||
def get_path_setting(setting_id):
|
|
||||||
with get_db() as session:
|
|
||||||
setting = session.query(PathSetting).get(setting_id)
|
|
||||||
if setting is None:
|
|
||||||
return jsonify({}), 204
|
|
||||||
return jsonify(setting.to_dict()), 200
|
|
||||||
|
|
||||||
@setting_bp.route('/path/', methods=['POST'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def create_path_setting():
|
|
||||||
data = request.json
|
|
||||||
webhook_setting_id = data.get('webhook_setting_id')
|
|
||||||
template_setting_id = data.get('template_setting_id')
|
|
||||||
setting = PathSetting(
|
|
||||||
webhook_setting_id=webhook_setting_id,
|
|
||||||
template_setting_id=template_setting_id
|
|
||||||
)
|
|
||||||
with get_db() as session:
|
|
||||||
try:
|
|
||||||
session.add(setting)
|
|
||||||
session.commit()
|
|
||||||
return jsonify(setting.to_dict()), 200
|
|
||||||
except Exception:
|
|
||||||
logger.error(f"failed to create path setting")
|
|
||||||
return jsonify({"error": "failed to create setting"}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@setting_bp.route('/path/<int:setting_id>', methods=['PUT', 'PATCH'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def update_path_setting(setting_id):
|
|
||||||
data = request.json
|
|
||||||
with get_db() as session:
|
|
||||||
try:
|
|
||||||
setting = session.query(PathSetting).get(setting_id)
|
|
||||||
if setting is None:
|
|
||||||
return jsonify({"error": "setting not exists"}), 400
|
|
||||||
webhook_setting_id = data.get('webhook_setting_id', setting.webhook_setting_id)
|
|
||||||
template_setting_id = data.get('template_setting_id', setting.template_setting_id)
|
|
||||||
setting.webhook_setting_id = webhook_setting_id
|
|
||||||
setting.template_setting_id = template_setting_id
|
|
||||||
session.commit()
|
|
||||||
return jsonify(setting.to_dict()), 200
|
|
||||||
except Exception:
|
|
||||||
return jsonify({"error": "failed to update path setting"}), 500
|
|
||||||
|
|
||||||
@setting_bp.route('/path/<int:setting_id>', methods=['DELETE'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def delete_path_setting(setting_id):
|
|
||||||
with get_db() as session:
|
|
||||||
setting = session.query(PathSetting).get(setting_id)
|
|
||||||
if setting is None:
|
|
||||||
return jsonify({"error": "setting not exists"}), 400
|
|
||||||
session.delete(setting)
|
|
||||||
return jsonify({"message": "deleted"}), 200
|
|
||||||
|
|
||||||
|
|
||||||
@setting_bp.route('/markdown/<int:setting_id>', methods=['GET'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
@etag_response
|
|
||||||
def get_markdown_path(setting_id):
|
|
||||||
with get_db() as session:
|
|
||||||
setting = session.query(MarkdownSetting).get(setting_id)
|
|
||||||
if setting is None:
|
|
||||||
return jsonify({}), 204
|
|
||||||
return jsonify(setting.to_dict()), 200
|
|
||||||
|
|
||||||
|
|
||||||
@setting_bp.route('/markdown/', methods=['POST'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def create_markdown_setting():
|
|
||||||
data = request.json
|
|
||||||
template_setting_id = data.get('template_setting_id')
|
|
||||||
setting = MarkdownSetting(template_setting_id=template_setting_id)
|
|
||||||
try:
|
|
||||||
with get_db() as session:
|
|
||||||
session.add(setting)
|
|
||||||
session.commit()
|
|
||||||
return jsonify(setting.to_dict()), 200
|
|
||||||
except Exception:
|
|
||||||
return jsonify({"error": "failed to create setting"}), 500
|
|
||||||
|
|
||||||
@setting_bp.route('/markdown/<int:setting_id>', methods=['PUT', 'PATCH'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def update_markdown_setting(setting_id):
|
|
||||||
data = request.json
|
|
||||||
try:
|
|
||||||
with get_db() as session:
|
|
||||||
setting = session.query(MarkdownSetting).get(setting_id)
|
|
||||||
if setting is None:
|
|
||||||
return jsonify({"error": "setting not exists"}), 400
|
|
||||||
template_setting_id = data.get('template_setting_id', setting.template_setting_id)
|
|
||||||
setting.template_setting_id = template_setting_id
|
|
||||||
session.commit()
|
|
||||||
return jsonify(setting.to_dict()), 200
|
|
||||||
except Exception:
|
|
||||||
return jsonify({"error": "failed to update setting"}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@setting_bp.route('/markdown/<int:setting_id>', methods=['DELETE'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def delete_markdown_setting(setting_id):
|
|
||||||
with get_db() as session:
|
|
||||||
setting = session.query(MarkdownSetting).get(setting_id)
|
|
||||||
if setting is None:
|
|
||||||
return jsonify({"error": "setting not exists"}), 400
|
|
||||||
session.delete(setting)
|
|
||||||
session.commit()
|
|
||||||
return jsonify({"message": "deleted"}), 200
|
|
||||||
8
api/setting/__init__.py
Normal file
8
api/setting/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import importlib
|
||||||
|
import pkgutil
|
||||||
|
|
||||||
|
from flask import Blueprint
|
||||||
|
setting_bp = Blueprint('setting', __name__, url_prefix='/api/setting')
|
||||||
|
|
||||||
|
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__, prefix=__name__ + "."):
|
||||||
|
importlib.import_module(module_name)
|
||||||
133
api/setting/markdown/__init__.py
Normal file
133
api/setting/markdown/__init__.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
from flask import jsonify, request, Blueprint
|
||||||
|
|
||||||
|
import api
|
||||||
|
from api import limiter, etag_response, require_auth
|
||||||
|
from db import get_db
|
||||||
|
from db.models.MarkdownSetting import MarkdownSetting
|
||||||
|
|
||||||
|
markdown_setting_bp = Blueprint('markdown_setting', __name__, url_prefix='/api/setting/markdown')
|
||||||
|
@markdown_setting_bp.route('/<int:setting_id>', methods=['GET'])
|
||||||
|
@limiter.limit(api.get_rate_limit)
|
||||||
|
@etag_response
|
||||||
|
def get_markdown_path(setting_id):
|
||||||
|
"""
|
||||||
|
Get a specific markdown setting by ID.
|
||||||
|
|
||||||
|
This endpoint retrieves a markdown setting by its ID.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the markdown setting to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the markdown setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
- 204: No content (setting not found)
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.query(MarkdownSetting).get(setting_id)
|
||||||
|
if setting is None:
|
||||||
|
return jsonify({}), 204
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@markdown_setting_bp.route('/', methods=['POST'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def create_markdown_setting():
|
||||||
|
"""
|
||||||
|
Create a new markdown setting.
|
||||||
|
|
||||||
|
This endpoint creates a new markdown setting with the provided template and permission settings.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- template_setting_id (int, optional): The ID of the template setting to associate
|
||||||
|
- permission_setting_id (int, optional): The ID of the permission setting to associate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the created markdown setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Created successfully
|
||||||
|
- 500: Server error
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
template_setting_id = data.get('template_setting_id')
|
||||||
|
permission_setting_id = data.get('permission_setting_id')
|
||||||
|
setting = MarkdownSetting(
|
||||||
|
template_setting_id=template_setting_id,
|
||||||
|
permission_setting_id=permission_setting_id
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with get_db() as session:
|
||||||
|
session.add(setting)
|
||||||
|
session.commit()
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
except Exception:
|
||||||
|
return jsonify({"error": "failed to create setting"}), 500
|
||||||
|
|
||||||
|
@markdown_setting_bp.route('/<int:setting_id>', methods=['PUT', 'PATCH'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def update_markdown_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Update a markdown setting.
|
||||||
|
|
||||||
|
This endpoint updates an existing markdown setting with the provided template and permission settings.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the markdown setting to update
|
||||||
|
- template_setting_id (int, optional): The new template setting ID
|
||||||
|
- permission_setting_id (int, optional): The new permission setting ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated markdown setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 400: Bad request (setting not found)
|
||||||
|
- 500: Server error
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
try:
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.query(MarkdownSetting).get(setting_id)
|
||||||
|
if setting is None:
|
||||||
|
return jsonify({"error": "setting not exists"}), 400
|
||||||
|
template_setting_id = data.get('template_setting_id', setting.template_setting_id)
|
||||||
|
permission_setting_id = data.get('permission_setting_id', setting.permission_setting_id)
|
||||||
|
setting.template_setting_id = template_setting_id
|
||||||
|
setting.permission_setting_id = permission_setting_id
|
||||||
|
session.commit()
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": "failed to update setting"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@markdown_setting_bp.route('/<int:setting_id>', methods=['DELETE'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def delete_markdown_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Delete a markdown setting.
|
||||||
|
|
||||||
|
This endpoint deletes an existing markdown setting.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the markdown setting to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object with a success message.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Deleted successfully
|
||||||
|
- 400: Bad request (setting not found)
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.query(MarkdownSetting).get(setting_id)
|
||||||
|
if setting is None:
|
||||||
|
return jsonify({"error": "setting not exists"}), 400
|
||||||
|
session.delete(setting)
|
||||||
|
session.commit()
|
||||||
|
return jsonify({"message": "deleted"}), 200
|
||||||
120
api/setting/markdown/permission.py
Normal file
120
api/setting/markdown/permission.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
from flask import jsonify, request, Blueprint
|
||||||
|
import api
|
||||||
|
from api import etag_response, limiter, require_auth
|
||||||
|
from db import get_db
|
||||||
|
from db.models.MarkdownPermissionSetting import MarkdownPermissionSetting
|
||||||
|
|
||||||
|
markdown_permission_setting_bp = Blueprint('markdown_permission_setting', __name__, url_prefix='/api/setting/markdown/permission')
|
||||||
|
@markdown_permission_setting_bp.route('/<int:setting_id>', methods=['GET'])
|
||||||
|
@etag_response
|
||||||
|
@limiter.limit(api.get_rate_limit)
|
||||||
|
def get_permission_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Get a specific markdown permission setting by ID.
|
||||||
|
|
||||||
|
This endpoint retrieves a markdown permission setting by its ID.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the permission setting to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the permission setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
- 204: No content (setting not found)
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.query(MarkdownPermissionSetting).get(setting_id)
|
||||||
|
if not setting:
|
||||||
|
return jsonify({}), 204
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@markdown_permission_setting_bp.route('/', methods=['POST'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def create_permission_setting():
|
||||||
|
"""
|
||||||
|
Create a new markdown permission setting.
|
||||||
|
|
||||||
|
This endpoint creates a new markdown permission setting with the provided permission value.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- permission (str): The permission value (e.g., 'private', 'protected', etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the created permission setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 201: Created successfully
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
permission = data.get('permission')
|
||||||
|
new_setting = MarkdownPermissionSetting(permission=permission)
|
||||||
|
with get_db() as session:
|
||||||
|
session.add(new_setting)
|
||||||
|
session.commit()
|
||||||
|
return jsonify(new_setting.to_dict()), 201
|
||||||
|
|
||||||
|
|
||||||
|
@markdown_permission_setting_bp.route('/<int:setting_id>', methods=['PUT', 'PATCH'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def update_permission_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Update a markdown permission setting.
|
||||||
|
|
||||||
|
This endpoint updates an existing markdown permission setting with the provided permission value.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
- PUT: Replaces the entire permission setting
|
||||||
|
- PATCH: Updates only the specified fields
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the permission setting to update
|
||||||
|
- permission (str): The new permission value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated permission setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 404: Permission setting not found
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.get(MarkdownPermissionSetting, setting_id)
|
||||||
|
if setting is None:
|
||||||
|
return jsonify({"error": "permission setting not found"}), 404
|
||||||
|
data = request.json
|
||||||
|
if request.method == 'PUT':
|
||||||
|
setting.permission = data.get('permission')
|
||||||
|
elif request.method == 'PATCH':
|
||||||
|
if 'permission' in data:
|
||||||
|
setting.permission = data.get('permission')
|
||||||
|
session.commit()
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
|
||||||
|
@markdown_permission_setting_bp.route('/<int:setting_id>', methods=['DELETE'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def delete_permission_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Delete a markdown permission setting.
|
||||||
|
|
||||||
|
This endpoint deletes an existing markdown permission setting.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the permission setting to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the deleted permission setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Deleted successfully
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.get(MarkdownPermissionSetting, setting_id)
|
||||||
|
st = setting.to_dict()
|
||||||
|
session.delete(setting)
|
||||||
|
session.commit()
|
||||||
|
return jsonify(st), 200
|
||||||
139
api/setting/markdown/template.py
Normal file
139
api/setting/markdown/template.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
from flask import jsonify, request, Blueprint
|
||||||
|
import api
|
||||||
|
from api import etag_response, limiter, require_auth
|
||||||
|
from db import get_db
|
||||||
|
from db.models.MarkdownTemplateSetting import MarkdownTemplateSetting
|
||||||
|
|
||||||
|
|
||||||
|
markdown_template_setting_bp = Blueprint('markdown_template_setting', __name__, url_prefix='/api/setting/markdown/template')
|
||||||
|
@markdown_template_setting_bp.route('/', methods=['GET'])
|
||||||
|
@etag_response
|
||||||
|
def list_template_settings():
|
||||||
|
"""
|
||||||
|
List all markdown template settings.
|
||||||
|
|
||||||
|
This endpoint retrieves a list of all markdown template settings in the system.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON array containing all template settings.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
settings = session.query(MarkdownTemplateSetting).all()
|
||||||
|
return jsonify([s.to_dict() for s in settings]), 200
|
||||||
|
|
||||||
|
@markdown_template_setting_bp.route('/<int:setting_id>', methods=['GET'])
|
||||||
|
@etag_response
|
||||||
|
@limiter.limit(api.get_rate_limit)
|
||||||
|
def get_template_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Get a specific markdown template setting by ID.
|
||||||
|
|
||||||
|
This endpoint retrieves a markdown template setting by its ID.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the template setting to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the template setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
- 204: No content (setting not found)
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.query(MarkdownTemplateSetting).get(setting_id)
|
||||||
|
if not setting:
|
||||||
|
return jsonify({}), 204
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@markdown_template_setting_bp.route('/', methods=['POST'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def create_template_setting():
|
||||||
|
"""
|
||||||
|
Create a new markdown template setting.
|
||||||
|
|
||||||
|
This endpoint creates a new markdown template setting with the provided template ID.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- template_id (int): The ID of the template to associate with this setting
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the created template setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 201: Created successfully
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
template_id = data.get('template_id')
|
||||||
|
new_setting = MarkdownTemplateSetting(template_id=template_id)
|
||||||
|
with get_db() as session:
|
||||||
|
session.add(new_setting)
|
||||||
|
session.commit()
|
||||||
|
return jsonify(new_setting.to_dict()), 201
|
||||||
|
|
||||||
|
|
||||||
|
@markdown_template_setting_bp.route('/<int:setting_id>', methods=['PUT', 'PATCH'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def update_template_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Update a markdown template setting.
|
||||||
|
|
||||||
|
This endpoint updates an existing markdown template setting with the provided template ID.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
- PUT: Replaces the entire template setting
|
||||||
|
- PATCH: Updates only the specified fields
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the template setting to update
|
||||||
|
- template_id (int): The new template ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated template setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 404: Template setting not found
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.get(MarkdownTemplateSetting, setting_id)
|
||||||
|
if setting is None:
|
||||||
|
return jsonify({"error": "template setting not found"}), 404
|
||||||
|
data = request.json
|
||||||
|
if request.method == 'PUT':
|
||||||
|
setting.template_id = data.get('template_id')
|
||||||
|
elif request.method == 'PATCH':
|
||||||
|
if 'template_id' in data:
|
||||||
|
setting.template_id = data.get('template_id')
|
||||||
|
session.commit()
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
|
||||||
|
@markdown_template_setting_bp.route('/<int:setting_id>', methods=['DELETE'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def delete_template_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Delete a markdown template setting.
|
||||||
|
|
||||||
|
This endpoint deletes an existing markdown template setting.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the template setting to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the deleted template setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Deleted successfully
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.get(MarkdownTemplateSetting, setting_id)
|
||||||
|
st = setting.to_dict()
|
||||||
|
session.delete(setting)
|
||||||
|
session.commit()
|
||||||
|
return jsonify(st), 200
|
||||||
136
api/setting/path/__init__.py
Normal file
136
api/setting/path/__init__.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
from flask import jsonify, request, Blueprint
|
||||||
|
|
||||||
|
import api
|
||||||
|
from api import limiter, require_auth, etag_response
|
||||||
|
from db import get_db
|
||||||
|
from db.models.PathSetting import PathSetting
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
path_setting_bp = Blueprint('path_setting', __name__, url_prefix='/api/setting/path')
|
||||||
|
|
||||||
|
@path_setting_bp.route('/<int:setting_id>', methods=['GET'])
|
||||||
|
@limiter.limit(api.get_rate_limit)
|
||||||
|
@etag_response
|
||||||
|
def get_path_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Get a specific path setting by ID.
|
||||||
|
|
||||||
|
This endpoint retrieves a path setting by its ID.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the path setting to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the path setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
- 204: No content (setting not found)
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.query(PathSetting).get(setting_id)
|
||||||
|
if setting is None:
|
||||||
|
return jsonify({}), 204
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
|
||||||
|
@path_setting_bp.route('/', methods=['POST'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def create_path_setting():
|
||||||
|
"""
|
||||||
|
Create a new path setting.
|
||||||
|
|
||||||
|
This endpoint creates a new path setting with the provided webhook and template settings.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- webhook_setting_id (int, optional): The ID of the webhook setting to associate
|
||||||
|
- template_setting_id (int, optional): The ID of the template setting to associate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the created path setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Created successfully
|
||||||
|
- 500: Server error
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
webhook_setting_id = data.get('webhook_setting_id')
|
||||||
|
template_setting_id = data.get('template_setting_id')
|
||||||
|
setting = PathSetting(
|
||||||
|
webhook_setting_id=webhook_setting_id,
|
||||||
|
template_setting_id=template_setting_id
|
||||||
|
)
|
||||||
|
with get_db() as session:
|
||||||
|
try:
|
||||||
|
session.add(setting)
|
||||||
|
session.commit()
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
except Exception:
|
||||||
|
logger.error(f"failed to create path setting")
|
||||||
|
return jsonify({"error": "failed to create setting"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@path_setting_bp.route('/<int:setting_id>', methods=['PUT', 'PATCH'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def update_path_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Update a path setting.
|
||||||
|
|
||||||
|
This endpoint updates an existing path setting with the provided webhook and template settings.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the path setting to update
|
||||||
|
- webhook_setting_id (int, optional): The new webhook setting ID
|
||||||
|
- template_setting_id (int, optional): The new template setting ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated path setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 400: Bad request (setting not found)
|
||||||
|
- 500: Server error
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
with get_db() as session:
|
||||||
|
try:
|
||||||
|
setting = session.query(PathSetting).get(setting_id)
|
||||||
|
if setting is None:
|
||||||
|
return jsonify({"error": "setting not exists"}), 400
|
||||||
|
webhook_setting_id = data.get('webhook_setting_id', setting.webhook_setting_id)
|
||||||
|
template_setting_id = data.get('template_setting_id', setting.template_setting_id)
|
||||||
|
setting.webhook_setting_id = webhook_setting_id
|
||||||
|
setting.template_setting_id = template_setting_id
|
||||||
|
session.commit()
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
except Exception:
|
||||||
|
return jsonify({"error": "failed to update path setting"}), 500
|
||||||
|
|
||||||
|
@path_setting_bp.route('/<int:setting_id>', methods=['DELETE'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def delete_path_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Delete a path setting.
|
||||||
|
|
||||||
|
This endpoint deletes an existing path setting.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the path setting to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object with a success message.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Deleted successfully
|
||||||
|
- 400: Bad request (setting not found)
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.query(PathSetting).get(setting_id)
|
||||||
|
if setting is None:
|
||||||
|
return jsonify({"error": "setting not exists"}), 400
|
||||||
|
session.delete(setting)
|
||||||
|
return jsonify({"message": "deleted"}), 200
|
||||||
159
api/setting/path/webhook.py
Normal file
159
api/setting/path/webhook.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
from flask import jsonify, request, Blueprint
|
||||||
|
from api import require_auth
|
||||||
|
from db import get_db
|
||||||
|
from db.models.WebhookSetting import WebhookSetting
|
||||||
|
|
||||||
|
path_webhook_setting_bp = Blueprint('path_webhook_setting', __name__, url_prefix='/api/setting/path/webhook')
|
||||||
|
@path_webhook_setting_bp.route('/', methods=['GET'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def list_webhook_settings():
|
||||||
|
"""
|
||||||
|
List all webhook settings.
|
||||||
|
|
||||||
|
This endpoint retrieves a list of all webhook settings in the system.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON array containing all webhook settings.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
settings = session.query(WebhookSetting).all()
|
||||||
|
return jsonify([s.to_dict() for s in settings]), 200
|
||||||
|
|
||||||
|
|
||||||
|
@path_webhook_setting_bp.route('/<int:setting_id>', methods=['GET'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def webhook_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Get a specific webhook setting by ID.
|
||||||
|
|
||||||
|
This endpoint retrieves a webhook setting by its ID.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the webhook setting to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the webhook setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
- 204: No content (setting not found)
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.query(WebhookSetting).filter(WebhookSetting.id == setting_id).first()
|
||||||
|
if not setting:
|
||||||
|
return jsonify({}), 204
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@path_webhook_setting_bp.route('/', methods=['POST'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def create_webhook_setting():
|
||||||
|
"""
|
||||||
|
Create a new webhook setting.
|
||||||
|
|
||||||
|
This endpoint creates a new webhook setting with the provided parameters.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- webhook_id (int): The ID of the webhook to associate
|
||||||
|
- recursive (bool, optional): Whether the webhook applies to subpaths (default: false)
|
||||||
|
- additional_header (str, optional): Additional headers for the webhook (default: '')
|
||||||
|
- enabled (bool, optional): Whether the webhook is enabled (default: true)
|
||||||
|
- on_events (int, optional): Event types that trigger the webhook (default: 1)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the created webhook setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 201: Created successfully
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
with get_db() as session:
|
||||||
|
setting = WebhookSetting(
|
||||||
|
webhook_id=data.get('webhook_id'),
|
||||||
|
recursive=data.get('recursive', False),
|
||||||
|
additional_header=data.get('additional_header', ''),
|
||||||
|
enabled=data.get('enabled', True),
|
||||||
|
on_events=data.get('on_events', 1),
|
||||||
|
)
|
||||||
|
session.add(setting)
|
||||||
|
session.commit()
|
||||||
|
return jsonify(setting.to_dict()), 201
|
||||||
|
|
||||||
|
|
||||||
|
@path_webhook_setting_bp.route('/<int:setting_id>', methods=['PUT', 'PATCH'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def update_webhook_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Update a webhook setting.
|
||||||
|
|
||||||
|
This endpoint updates an existing webhook setting with the provided parameters.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the webhook setting to update
|
||||||
|
- webhook_id (int, optional): The new webhook ID
|
||||||
|
- recursive (bool, optional): Whether the webhook applies to subpaths
|
||||||
|
- additional_header (str, optional): Additional headers for the webhook
|
||||||
|
- enabled (bool, optional): Whether the webhook is enabled
|
||||||
|
- on_events (int, optional): Event types that trigger the webhook
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated webhook setting.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 404: Webhook setting not found
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.query(WebhookSetting).get(setting_id)
|
||||||
|
if not setting:
|
||||||
|
return jsonify({'error': 'Webhook setting not found'}), 404
|
||||||
|
if 'webhook_id' in data:
|
||||||
|
setting.webhook_id = data['webhook_id']
|
||||||
|
if 'recursive' in data:
|
||||||
|
setting.recursive = data['recursive']
|
||||||
|
if 'additional_header' in data:
|
||||||
|
setting.additional_header = data['additional_header']
|
||||||
|
if 'enabled' in data:
|
||||||
|
setting.enabled = data['enabled']
|
||||||
|
if 'on_events' in data:
|
||||||
|
setting.on_events = data['on_events']
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
return jsonify(setting.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@path_webhook_setting_bp.route('/<int:setting_id>', methods=['DELETE'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def delete_webhook_setting(setting_id):
|
||||||
|
"""
|
||||||
|
Delete a webhook setting.
|
||||||
|
|
||||||
|
This endpoint deletes an existing webhook setting.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- setting_id (int): The ID of the webhook setting to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object with a success message.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Deleted successfully
|
||||||
|
- 404: Webhook setting not found
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
setting = session.query(WebhookSetting).get(setting_id)
|
||||||
|
if not setting:
|
||||||
|
return jsonify({'error': 'Webhook setting not found'}), 404
|
||||||
|
|
||||||
|
session.delete(setting)
|
||||||
|
session.commit()
|
||||||
|
return jsonify({'message': 'Webhook setting deleted'}), 200
|
||||||
136
api/template.py
136
api/template.py
@@ -1,136 +0,0 @@
|
|||||||
from flask import Blueprint, jsonify, request
|
|
||||||
from db import get_db
|
|
||||||
from api import require_auth, etag_response
|
|
||||||
from db.models.MarkdownTemplate import MarkdownTemplate
|
|
||||||
from db.models.PathTemplate import PathTemplate
|
|
||||||
|
|
||||||
template_bp = Blueprint('template', __name__, url_prefix='/api/template')
|
|
||||||
|
|
||||||
@template_bp.route('/path/<int:template_id>', methods=['GET'])
|
|
||||||
@etag_response
|
|
||||||
def get_path_template(template_id):
|
|
||||||
with get_db() as session:
|
|
||||||
template = session.query(PathTemplate).get(template_id)
|
|
||||||
if template is None:
|
|
||||||
return jsonify({}), 204
|
|
||||||
return jsonify(template.to_dict()), 200
|
|
||||||
|
|
||||||
@template_bp.route('/path/', methods=['GET'])
|
|
||||||
@etag_response
|
|
||||||
def get_path_templates():
|
|
||||||
with get_db() as session:
|
|
||||||
templates = session.query(PathTemplate).all()
|
|
||||||
return jsonify([template.to_dict() for template in templates]), 200
|
|
||||||
|
|
||||||
|
|
||||||
@template_bp.route('/path/', methods=['POST'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def create_path_template():
|
|
||||||
data = request.json
|
|
||||||
if "title" not in data:
|
|
||||||
return jsonify({"error": "title is missing"}), 400
|
|
||||||
title = data.get("title")
|
|
||||||
structure = data.get("structure")
|
|
||||||
template = PathTemplate(title=title, structure=structure)
|
|
||||||
try:
|
|
||||||
with get_db() as session:
|
|
||||||
session.add(template)
|
|
||||||
session.commit()
|
|
||||||
return template.to_dict(), 200
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": "failed to create path template"}), 400
|
|
||||||
|
|
||||||
|
|
||||||
@template_bp.route('/path/<int:template_id>', methods=['PUT', 'PATCH'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def update_path_template(template_id):
|
|
||||||
data = request.json
|
|
||||||
with get_db() as session:
|
|
||||||
template = session.query(PathTemplate).get(template_id)
|
|
||||||
if template is None:
|
|
||||||
return jsonify({'error': 'path template not found'}), 400
|
|
||||||
title = data.get("title", template.title)
|
|
||||||
structure = data.get("structure", template.structure)
|
|
||||||
template.title = title
|
|
||||||
template.structure = structure
|
|
||||||
session.commit()
|
|
||||||
return jsonify(template.to_dict()), 200
|
|
||||||
|
|
||||||
|
|
||||||
@template_bp.route('/path/<int:template_id>', methods=['DELETE'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def delete_path_template(template_id):
|
|
||||||
with get_db() as session:
|
|
||||||
template = session.query(PathTemplate).get(template_id)
|
|
||||||
if template is None:
|
|
||||||
return jsonify({'error': 'path template not found'}), 400
|
|
||||||
session.delete(template)
|
|
||||||
session.commit()
|
|
||||||
return jsonify({'message': 'deleted'}), 200
|
|
||||||
|
|
||||||
|
|
||||||
@template_bp.route('/markdown/<int:template_id>', methods=['GET'])
|
|
||||||
@etag_response
|
|
||||||
def get_markdown_template(template_id):
|
|
||||||
with get_db() as session:
|
|
||||||
template = session.query(MarkdownTemplate).get(template_id)
|
|
||||||
if template is None:
|
|
||||||
return jsonify({}), 204
|
|
||||||
return jsonify(template.to_dict()), 200
|
|
||||||
|
|
||||||
@template_bp.route('/markdown/', methods=['GET'])
|
|
||||||
@etag_response
|
|
||||||
def get_markdown_templates():
|
|
||||||
with get_db() as session:
|
|
||||||
templates = session.query(MarkdownTemplate).all()
|
|
||||||
return jsonify([template.to_dict() for template in templates]), 200
|
|
||||||
|
|
||||||
|
|
||||||
@template_bp.route('/markdown/', methods=['POST'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def create_markdown_template():
|
|
||||||
data = request.json
|
|
||||||
if "title" not in data:
|
|
||||||
return jsonify({"error": "title is missing"}), 400
|
|
||||||
title = data.get("title")
|
|
||||||
parameters = data.get("parameters")
|
|
||||||
define = data.get("define")
|
|
||||||
template = MarkdownTemplate(title=title, parameters=parameters, define=define)
|
|
||||||
try:
|
|
||||||
with get_db() as session:
|
|
||||||
session.add(template)
|
|
||||||
session.commit()
|
|
||||||
return template.to_dict(), 200
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": "failed to create markdown template"}), 400
|
|
||||||
|
|
||||||
|
|
||||||
@template_bp.route('/markdown/<int:template_id>', methods=['PUT', 'PATCH'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def update_markdown_template(template_id):
|
|
||||||
data = request.json
|
|
||||||
with get_db() as session:
|
|
||||||
template = session.query(MarkdownTemplate).get(template_id)
|
|
||||||
if template is None:
|
|
||||||
return jsonify({'error': 'markdown template not found'}), 400
|
|
||||||
title = data.get("title", template.title)
|
|
||||||
parameters = data.get("parameters", template.parameters)
|
|
||||||
define = data.get("define", template.define)
|
|
||||||
template.title = title
|
|
||||||
template.parameters = parameters
|
|
||||||
template.define = define
|
|
||||||
session.commit()
|
|
||||||
return jsonify(template.to_dict()), 200
|
|
||||||
|
|
||||||
|
|
||||||
@template_bp.route('/markdown/<int:template_id>', methods=['DELETE'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def delete_markdown_template(template_id):
|
|
||||||
with get_db() as session:
|
|
||||||
template = session.query(MarkdownTemplate).get(template_id)
|
|
||||||
if template is None:
|
|
||||||
return jsonify({'error': 'markdown template not found'}), 400
|
|
||||||
session.delete(template)
|
|
||||||
session.commit()
|
|
||||||
return jsonify({'message': 'deleted'}), 200
|
|
||||||
|
|
||||||
9
api/template/__init__.py
Normal file
9
api/template/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import importlib
|
||||||
|
import pkgutil
|
||||||
|
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
template_bp = Blueprint('template', __name__, url_prefix='/api/template')
|
||||||
|
|
||||||
|
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__, prefix=__name__ + "."):
|
||||||
|
importlib.import_module(module_name)
|
||||||
172
api/template/markdown/__init__.py
Normal file
172
api/template/markdown/__init__.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
from flask import jsonify, request, Blueprint
|
||||||
|
from api import etag_response, require_auth
|
||||||
|
from db import get_db
|
||||||
|
from db.models.MarkdownTemplate import MarkdownTemplate
|
||||||
|
|
||||||
|
cached_templates = {}
|
||||||
|
|
||||||
|
markdown_template_bp = Blueprint('markdown_template', __name__, url_prefix='/api/template/markdown')
|
||||||
|
|
||||||
|
def inflate_template(template):
|
||||||
|
for parameter in template.get('parameters'):
|
||||||
|
if parameter.get('type', {}).get('base_type') == 'template':
|
||||||
|
sub_template_id = parameter.get('type', {}).get('definition', {}).get('template', {}).get('id', 0)
|
||||||
|
if sub_template_id in cached_templates.keys():
|
||||||
|
parameter['type']['definition']['template'] = cached_templates[sub_template_id]
|
||||||
|
else:
|
||||||
|
with get_db() as session:
|
||||||
|
sub_template = session.query(MarkdownTemplate).get(sub_template_id)
|
||||||
|
parameter['type']['definition']['template'] = inflate_template(sub_template)
|
||||||
|
cached_templates[template['id']] = template
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
@markdown_template_bp.route('/<int:template_id>', methods=['GET'])
|
||||||
|
@etag_response
|
||||||
|
def get_markdown_template(template_id):
|
||||||
|
"""
|
||||||
|
Get a specific markdown template by ID.
|
||||||
|
|
||||||
|
This endpoint retrieves a markdown template by its ID.
|
||||||
|
The template is inflated to include any nested templates.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- template_id (int): The ID of the template to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the markdown template.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
- 204: No content (template not found)
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
template = session.query(MarkdownTemplate).get(template_id)
|
||||||
|
if template is None:
|
||||||
|
return jsonify({}), 204
|
||||||
|
return jsonify(inflate_template(template.to_dict())), 200
|
||||||
|
|
||||||
|
@markdown_template_bp.route('/', methods=['GET'])
|
||||||
|
@etag_response
|
||||||
|
def get_markdown_templates():
|
||||||
|
"""
|
||||||
|
List all markdown templates.
|
||||||
|
|
||||||
|
This endpoint retrieves a list of all markdown templates in the system.
|
||||||
|
Each template is inflated to include any nested templates.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON array containing all markdown templates.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
templates = session.query(MarkdownTemplate).all()
|
||||||
|
return jsonify([inflate_template(template.to_dict()) for template in templates]), 200
|
||||||
|
|
||||||
|
|
||||||
|
@markdown_template_bp.route('/', methods=['POST'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def create_markdown_template():
|
||||||
|
"""
|
||||||
|
Create a new markdown template.
|
||||||
|
|
||||||
|
This endpoint creates a new markdown template with the provided data.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- title (str): The title of the template (required)
|
||||||
|
- parameters (object, optional): The parameters for the template
|
||||||
|
- layout (str, optional): The layout content of the template
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the created template.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Created successfully
|
||||||
|
- 400: Bad request (missing title or other error)
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
if "title" not in data:
|
||||||
|
return jsonify({"error": "title is missing"}), 400
|
||||||
|
title = data.get("title")
|
||||||
|
parameters = data.get("parameters")
|
||||||
|
layout = data.get("layout")
|
||||||
|
template = MarkdownTemplate(title=title, parameters=parameters, layout=layout)
|
||||||
|
try:
|
||||||
|
with get_db() as session:
|
||||||
|
session.add(template)
|
||||||
|
session.commit()
|
||||||
|
return jsonify(template.to_dict()), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": "failed to create markdown template"}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@markdown_template_bp.route('/<int:template_id>', methods=['PUT', 'PATCH'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def update_markdown_template(template_id):
|
||||||
|
"""
|
||||||
|
Update a markdown template.
|
||||||
|
|
||||||
|
This endpoint updates an existing markdown template with the provided data.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- template_id (int): The ID of the template to update
|
||||||
|
- title (str, optional): The new title for the template
|
||||||
|
- parameters (object, optional): The new parameters for the template
|
||||||
|
- layout (str, optional): The new layout content for the template
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated template.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 400: Bad request (template not found)
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
with get_db() as session:
|
||||||
|
template = session.query(MarkdownTemplate).get(template_id)
|
||||||
|
if template is None:
|
||||||
|
return jsonify({'error': 'markdown template not found'}), 400
|
||||||
|
title = data.get("title", template.title)
|
||||||
|
parameters = data.get("parameters", template.parameters)
|
||||||
|
layout = data.get("layout", template.layout)
|
||||||
|
template.title = title
|
||||||
|
template.parameters = parameters
|
||||||
|
template.layout = layout
|
||||||
|
session.commit()
|
||||||
|
if template_id in cached_templates.keys():
|
||||||
|
cached_templates[template_id] = inflate_template(template.to_dict())
|
||||||
|
return jsonify(template.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@markdown_template_bp.route('/<int:template_id>', methods=['DELETE'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def delete_markdown_template(template_id):
|
||||||
|
"""
|
||||||
|
Delete a markdown template.
|
||||||
|
|
||||||
|
This endpoint deletes an existing markdown template.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- template_id (int): The ID of the template to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object with a success message.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Deleted successfully
|
||||||
|
- 400: Bad request (template not found)
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
template = session.query(MarkdownTemplate).get(template_id)
|
||||||
|
if template is None:
|
||||||
|
return jsonify({'error': 'markdown template not found'}), 400
|
||||||
|
session.delete(template)
|
||||||
|
session.commit()
|
||||||
|
if template_id in cached_templates.keys():
|
||||||
|
cached_templates.pop(template_id)
|
||||||
|
return jsonify({'message': 'deleted'}), 200
|
||||||
147
api/template/path/__init__.py
Normal file
147
api/template/path/__init__.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
from flask.sansio.blueprints import Blueprint
|
||||||
|
|
||||||
|
from api import etag_response, require_auth
|
||||||
|
from api.template import template_bp
|
||||||
|
from db import get_db
|
||||||
|
from db.models.PathTemplate import PathTemplate
|
||||||
|
|
||||||
|
path_template_bp = Blueprint('path_template', __name__, url_prefix='/api/template/path')
|
||||||
|
@path_template_bp.route('/<int:template_id>', methods=['GET'])
|
||||||
|
@etag_response
|
||||||
|
def get_path_template(template_id):
|
||||||
|
"""
|
||||||
|
Get a specific path template by ID.
|
||||||
|
|
||||||
|
This endpoint retrieves a path template by its ID.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- template_id (int): The ID of the template to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the path template.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
- 204: No content (template not found)
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
template = session.query(PathTemplate).get(template_id)
|
||||||
|
if template is None:
|
||||||
|
return jsonify({}), 204
|
||||||
|
return jsonify(template.to_dict()), 200
|
||||||
|
|
||||||
|
@path_template_bp.route('/', methods=['GET'])
|
||||||
|
@etag_response
|
||||||
|
def get_path_templates():
|
||||||
|
"""
|
||||||
|
List all path templates.
|
||||||
|
|
||||||
|
This endpoint retrieves a list of all path templates in the system.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON array containing all path templates.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
templates = session.query(PathTemplate).all()
|
||||||
|
return jsonify([template.to_dict() for template in templates]), 200
|
||||||
|
|
||||||
|
|
||||||
|
@path_template_bp.route('/', methods=['POST'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def create_path_template():
|
||||||
|
"""
|
||||||
|
Create a new path template.
|
||||||
|
|
||||||
|
This endpoint creates a new path template with the provided data.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- title (str): The title of the template (required)
|
||||||
|
- structure (str): The structure definition for the template
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the created template.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Created successfully
|
||||||
|
- 400: Bad request (missing title or other error)
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
if "title" not in data:
|
||||||
|
return jsonify({"error": "title is missing"}), 400
|
||||||
|
title = data.get("title")
|
||||||
|
structure = data.get("structure")
|
||||||
|
template = PathTemplate(title=title, structure=structure)
|
||||||
|
try:
|
||||||
|
with get_db() as session:
|
||||||
|
session.add(template)
|
||||||
|
session.commit()
|
||||||
|
return template.to_dict(), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": "failed to create path template"}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@path_template_bp.route('/<int:template_id>', methods=['PUT', 'PATCH'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def update_path_template(template_id):
|
||||||
|
"""
|
||||||
|
Update a path template.
|
||||||
|
|
||||||
|
This endpoint updates an existing path template with the provided data.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- template_id (int): The ID of the template to update
|
||||||
|
- title (str, optional): The new title for the template
|
||||||
|
- structure (str, optional): The new structure definition for the template
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated template.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 400: Bad request (template not found)
|
||||||
|
"""
|
||||||
|
data = request.json
|
||||||
|
with get_db() as session:
|
||||||
|
template = session.query(PathTemplate).get(template_id)
|
||||||
|
if template is None:
|
||||||
|
return jsonify({'error': 'path template not found'}), 400
|
||||||
|
title = data.get("title", template.title)
|
||||||
|
structure = data.get("structure", template.structure)
|
||||||
|
template.title = title
|
||||||
|
template.structure = structure
|
||||||
|
session.commit()
|
||||||
|
return jsonify(template.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@path_template_bp.route('/<int:template_id>', methods=['DELETE'])
|
||||||
|
@require_auth(roles=['admin'])
|
||||||
|
def delete_path_template(template_id):
|
||||||
|
"""
|
||||||
|
Delete a path template.
|
||||||
|
|
||||||
|
This endpoint deletes an existing path template.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- template_id (int): The ID of the template to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object with a success message.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Deleted successfully
|
||||||
|
- 400: Bad request (template not found)
|
||||||
|
"""
|
||||||
|
with get_db() as session:
|
||||||
|
template = session.query(PathTemplate).get(template_id)
|
||||||
|
if template is None:
|
||||||
|
return jsonify({'error': 'path template not found'}), 400
|
||||||
|
session.delete(template)
|
||||||
|
session.commit()
|
||||||
|
return jsonify({'message': 'deleted'}), 200
|
||||||
82
api/tree.py
82
api/tree.py
@@ -1,11 +1,14 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, jsonify
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
import api
|
import api
|
||||||
from api import require_auth, etag_response
|
from api import etag_response, is_user_admin
|
||||||
from db import get_db
|
from db import get_db
|
||||||
from db.models.Markdown import Markdown
|
from db.models.Markdown import Markdown
|
||||||
from db.models.Path import Path
|
from db.models.Path import Path
|
||||||
|
from db.models.MarkdownSetting import MarkdownSetting
|
||||||
|
from db.models.MarkdownPermissionSetting import MarkdownPermissionSetting
|
||||||
from api import limiter
|
from api import limiter
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -13,9 +16,33 @@ logger = logging.getLogger(__name__)
|
|||||||
tree_bp = Blueprint('tree', __name__, url_prefix='/api/tree')
|
tree_bp = Blueprint('tree', __name__, url_prefix='/api/tree')
|
||||||
|
|
||||||
|
|
||||||
def build_tree(db: Session, parent_id: int = None):
|
def build_tree(db: Session, parent_id: int = None, is_admin=False):
|
||||||
path_nodes = db.query(Path).filter(Path.parent_id == parent_id).all()
|
path_nodes = db.query(Path).filter(Path.parent_id == parent_id).all()
|
||||||
md_nodes = db.query(Markdown.id, Markdown.title, Markdown.order, Markdown.shortcut, Markdown.setting_id).filter(Markdown.path_id == parent_id).all()
|
|
||||||
|
markdown_query = db.query(
|
||||||
|
Markdown.id,
|
||||||
|
Markdown.title,
|
||||||
|
Markdown.order,
|
||||||
|
Markdown.shortcut,
|
||||||
|
Markdown.setting_id
|
||||||
|
).filter(Markdown.path_id == parent_id)
|
||||||
|
|
||||||
|
if not is_admin:
|
||||||
|
markdown_query = markdown_query.outerjoin(
|
||||||
|
MarkdownSetting,
|
||||||
|
Markdown.setting_id == MarkdownSetting.id
|
||||||
|
).outerjoin(
|
||||||
|
MarkdownPermissionSetting,
|
||||||
|
MarkdownSetting.permission_setting_id == MarkdownPermissionSetting.id
|
||||||
|
).filter(
|
||||||
|
or_(
|
||||||
|
MarkdownPermissionSetting.permission != 'private',
|
||||||
|
MarkdownPermissionSetting.permission == None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
md_nodes = markdown_query.all()
|
||||||
|
|
||||||
t0 = [
|
t0 = [
|
||||||
{
|
{
|
||||||
"id": node.id,
|
"id": node.id,
|
||||||
@@ -26,7 +53,7 @@ def build_tree(db: Session, parent_id: int = None):
|
|||||||
} for node in md_nodes
|
} for node in md_nodes
|
||||||
]
|
]
|
||||||
t1 = [
|
t1 = [
|
||||||
{**node.to_dict(), "type": "path", "children": build_tree(db, node.id)} for node in path_nodes
|
{**node.to_dict(), "type": "path", "children": build_tree(db, node.id, is_admin)} for node in path_nodes
|
||||||
]
|
]
|
||||||
for node in t1:
|
for node in t1:
|
||||||
for child in node["children"]:
|
for child in node["children"]:
|
||||||
@@ -39,8 +66,49 @@ def build_tree(db: Session, parent_id: int = None):
|
|||||||
@limiter.limit(api.get_rate_limit)
|
@limiter.limit(api.get_rate_limit)
|
||||||
@etag_response
|
@etag_response
|
||||||
def get_tree():
|
def get_tree():
|
||||||
|
"""
|
||||||
|
Get the complete tree structure of paths and markdowns.
|
||||||
|
|
||||||
|
This endpoint retrieves the hierarchical tree structure of all paths and markdowns.
|
||||||
|
For non-admin users, markdowns with 'private' permission settings are filtered out.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object representing the tree structure with the following format:
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "",
|
||||||
|
"parent_id": null,
|
||||||
|
"order": "...",
|
||||||
|
"setting_id": null,
|
||||||
|
"type": "path",
|
||||||
|
"index": true/false,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": ...,
|
||||||
|
"title": "...",
|
||||||
|
"order": "...",
|
||||||
|
"setting_id": null,
|
||||||
|
"type": "markdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": ...,
|
||||||
|
"name": "...",
|
||||||
|
"parent_id": 1,
|
||||||
|
"order": "...",
|
||||||
|
"setting_id": null,
|
||||||
|
"type": "path",
|
||||||
|
"children": [...]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
|
is_admin = is_user_admin()
|
||||||
|
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
children = build_tree(session, 1)
|
children = build_tree(session, 1, is_admin)
|
||||||
root = session.query(Path).get(1)
|
root = session.query(Path).get(1)
|
||||||
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
@@ -50,4 +118,4 @@ def get_tree():
|
|||||||
"index": any("title" in child.keys() and child["title"] == "index" for child in children),
|
"index": any("title" in child.keys() and child["title"] == "index" for child in children),
|
||||||
"children": children
|
"children": children
|
||||||
}
|
}
|
||||||
), 200
|
), 200
|
||||||
|
|||||||
139
api/webhook.py
139
api/webhook.py
@@ -2,7 +2,6 @@ from flask import Blueprint, jsonify, request
|
|||||||
from api import require_auth
|
from api import require_auth
|
||||||
from db import get_db
|
from db import get_db
|
||||||
from db.models.Webhook import Webhook
|
from db.models.Webhook import Webhook
|
||||||
from db.models.WebhookSetting import WebhookSetting
|
|
||||||
|
|
||||||
webhook_bp = Blueprint('webhook', __name__, url_prefix='/api/webhook')
|
webhook_bp = Blueprint('webhook', __name__, url_prefix='/api/webhook')
|
||||||
|
|
||||||
@@ -10,6 +9,18 @@ webhook_bp = Blueprint('webhook', __name__, url_prefix='/api/webhook')
|
|||||||
@webhook_bp.route('/', methods=['GET'])
|
@webhook_bp.route('/', methods=['GET'])
|
||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
def list_webhooks():
|
def list_webhooks():
|
||||||
|
"""
|
||||||
|
List all webhooks.
|
||||||
|
|
||||||
|
This endpoint retrieves a list of all webhooks in the system.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON array containing all webhooks.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Success
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
hooks = session.query(Webhook).all()
|
hooks = session.query(Webhook).all()
|
||||||
return jsonify([h.to_dict() for h in hooks]), 200
|
return jsonify([h.to_dict() for h in hooks]), 200
|
||||||
@@ -18,6 +29,23 @@ def list_webhooks():
|
|||||||
@webhook_bp.route('/', methods=['POST'])
|
@webhook_bp.route('/', methods=['POST'])
|
||||||
@require_auth(['admin'])
|
@require_auth(['admin'])
|
||||||
def create_webhook():
|
def create_webhook():
|
||||||
|
"""
|
||||||
|
Create a new webhook.
|
||||||
|
|
||||||
|
This endpoint creates a new webhook with the provided URL.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- hook_url (str): The URL of the webhook
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the created webhook.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 201: Created successfully
|
||||||
|
- 400: Bad request (hook_url is missing)
|
||||||
|
- 409: Conflict (webhook URL already exists)
|
||||||
|
"""
|
||||||
data = request.json
|
data = request.json
|
||||||
hook_url = data.get('hook_url')
|
hook_url = data.get('hook_url')
|
||||||
if not hook_url:
|
if not hook_url:
|
||||||
@@ -33,12 +61,28 @@ def create_webhook():
|
|||||||
return jsonify(webhook.to_dict()), 201
|
return jsonify(webhook.to_dict()), 201
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@webhook_bp.route('/<int:webhook_id>', methods=['PUT', 'PATCH'])
|
@webhook_bp.route('/<int:webhook_id>', methods=['PUT', 'PATCH'])
|
||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
def update_webhook(webhook_id):
|
def update_webhook(webhook_id):
|
||||||
|
"""
|
||||||
|
Update a webhook.
|
||||||
|
|
||||||
|
This endpoint updates an existing webhook with the provided URL.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- webhook_id (int): The ID of the webhook to update
|
||||||
|
- hook_url (str): The new URL for the webhook
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object containing the updated webhook.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Updated successfully
|
||||||
|
- 400: Bad request (hook_url is missing)
|
||||||
|
- 404: Webhook not found
|
||||||
|
- 409: Conflict (webhook URL already exists)
|
||||||
|
"""
|
||||||
data = request.json
|
data = request.json
|
||||||
if 'hook_url' not in data:
|
if 'hook_url' not in data:
|
||||||
return jsonify({'error': 'hook_url is required'}), 400
|
return jsonify({'error': 'hook_url is required'}), 400
|
||||||
@@ -60,6 +104,22 @@ def update_webhook(webhook_id):
|
|||||||
@webhook_bp.route('/<int:webhook_id>', methods=['DELETE'])
|
@webhook_bp.route('/<int:webhook_id>', methods=['DELETE'])
|
||||||
@require_auth(roles=['admin'])
|
@require_auth(roles=['admin'])
|
||||||
def delete_webhook(webhook_id):
|
def delete_webhook(webhook_id):
|
||||||
|
"""
|
||||||
|
Delete a webhook.
|
||||||
|
|
||||||
|
This endpoint deletes an existing webhook.
|
||||||
|
It requires authentication with the 'admin' role.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- webhook_id (int): The ID of the webhook to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A JSON object with a success message.
|
||||||
|
|
||||||
|
Response Codes:
|
||||||
|
- 200: Deleted successfully
|
||||||
|
- 404: Webhook not found
|
||||||
|
"""
|
||||||
with get_db() as session:
|
with get_db() as session:
|
||||||
webhook = session.query(Webhook).get(webhook_id)
|
webhook = session.query(Webhook).get(webhook_id)
|
||||||
if not webhook:
|
if not webhook:
|
||||||
@@ -68,74 +128,3 @@ def delete_webhook(webhook_id):
|
|||||||
session.delete(webhook)
|
session.delete(webhook)
|
||||||
session.commit()
|
session.commit()
|
||||||
return jsonify({'message': 'Webhook deleted'}), 200
|
return jsonify({'message': 'Webhook deleted'}), 200
|
||||||
|
|
||||||
|
|
||||||
@webhook_bp.route('/setting/', methods=['GET'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def list_webhook_settings():
|
|
||||||
with get_db() as session:
|
|
||||||
settings = session.query(WebhookSetting).all()
|
|
||||||
return jsonify([s.to_dict() for s in settings]), 200
|
|
||||||
|
|
||||||
|
|
||||||
@webhook_bp.route('/setting/<int:setting_id>', methods=['GET'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def webhook_setting(setting_id):
|
|
||||||
with get_db() as session:
|
|
||||||
setting = session.query(WebhookSetting).filter(WebhookSetting.id == setting_id).first()
|
|
||||||
if not setting:
|
|
||||||
return jsonify({'info': 'Webhook setting not found'}), 204
|
|
||||||
return jsonify(setting.to_dict()), 200
|
|
||||||
|
|
||||||
|
|
||||||
@webhook_bp.route('/setting/', methods=['POST'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def create_webhook_setting():
|
|
||||||
data = request.json
|
|
||||||
with get_db() as session:
|
|
||||||
setting = WebhookSetting(
|
|
||||||
webhook_id=data.get('webhook_id'),
|
|
||||||
recursive=data.get('recursive', False),
|
|
||||||
additional_header=data.get('additional_header', ''),
|
|
||||||
enabled=data.get('enabled', True),
|
|
||||||
on_events=data.get('on_events', 1),
|
|
||||||
)
|
|
||||||
session.add(setting)
|
|
||||||
session.commit()
|
|
||||||
return jsonify(setting.to_dict()), 201
|
|
||||||
|
|
||||||
|
|
||||||
@webhook_bp.route('/setting/<int:setting_id>', methods=['PUT', 'PATCH'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def update_webhook_setting(setting_id):
|
|
||||||
data = request.json
|
|
||||||
with get_db() as session:
|
|
||||||
setting = session.query(WebhookSetting).get(setting_id)
|
|
||||||
if not setting:
|
|
||||||
return jsonify({'error': 'Webhook setting not found'}), 404
|
|
||||||
if 'webhook_id' in data:
|
|
||||||
setting.webhook_id = data['webhook_id']
|
|
||||||
if 'recursive' in data:
|
|
||||||
setting.recursive = data['recursive']
|
|
||||||
if 'additional_header' in data:
|
|
||||||
setting.additional_header = data['additional_header']
|
|
||||||
if 'enabled' in data:
|
|
||||||
setting.enabled = data['enabled']
|
|
||||||
if 'on_events' in data:
|
|
||||||
setting.on_events = data['on_events']
|
|
||||||
|
|
||||||
session.commit()
|
|
||||||
return jsonify(setting.to_dict()), 200
|
|
||||||
|
|
||||||
|
|
||||||
@webhook_bp.route('/setting/<int:setting_id>', methods=['DELETE'])
|
|
||||||
@require_auth(roles=['admin'])
|
|
||||||
def delete_webhook_setting(setting_id):
|
|
||||||
with get_db() as session:
|
|
||||||
setting = session.query(WebhookSetting).get(setting_id)
|
|
||||||
if not setting:
|
|
||||||
return jsonify({'error': 'Webhook setting not found'}), 404
|
|
||||||
|
|
||||||
session.delete(setting)
|
|
||||||
session.commit()
|
|
||||||
return jsonify({'message': 'Webhook setting deleted'}), 200
|
|
||||||
|
|||||||
13
app.py
13
app.py
@@ -1,4 +1,6 @@
|
|||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
|
from events.WebhookEventHandlers import register_all_webhook_event_handlers
|
||||||
from logging_handlers.DatabaseLogHandler import DatabaseLogHandler
|
from logging_handlers.DatabaseLogHandler import DatabaseLogHandler
|
||||||
from api import limiter
|
from api import limiter
|
||||||
from flask import Flask, request
|
from flask import Flask, request
|
||||||
@@ -33,7 +35,8 @@ CORS(app, resources={
|
|||||||
r"https?://localhost:\d+",
|
r"https?://localhost:\d+",
|
||||||
r"https?://127\.0\.0\.1:\d+",
|
r"https?://127\.0\.0\.1:\d+",
|
||||||
r"https?://localhost"
|
r"https?://localhost"
|
||||||
]
|
],
|
||||||
|
"supports_credentials": True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
expose_headers=['Content-Disposition']
|
expose_headers=['Content-Disposition']
|
||||||
@@ -42,7 +45,7 @@ CORS(app, resources={
|
|||||||
limiter.init_app(app)
|
limiter.init_app(app)
|
||||||
|
|
||||||
api.register_blueprints(app)
|
api.register_blueprints(app)
|
||||||
|
register_all_webhook_event_handlers()
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def log_request():
|
def log_request():
|
||||||
if request.path.startswith("/api/log"):
|
if request.path.startswith("/api/log"):
|
||||||
@@ -50,10 +53,8 @@ def log_request():
|
|||||||
logger.info(f"Request received: {request.method} {request.path} from {request.remote_addr}")
|
logger.info(f"Request received: {request.method} {request.path} from {request.remote_addr}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
api.init_rate_limits(app)
|
api.init_rate_limits(app)
|
||||||
#logger.info("Starting app")
|
print("env")
|
||||||
pprint(env_provider.summerize())
|
pprint(env_provider.summerize())
|
||||||
app.run(host='0.0.0.0', port=5000)
|
app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=True)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from sqlalchemy.orm import sessionmaker
|
|||||||
from sqlalchemy.dialects.mysql import insert
|
from sqlalchemy.dialects.mysql import insert
|
||||||
from sqlalchemy import create_engine, text, inspect
|
from sqlalchemy import create_engine, text, inspect
|
||||||
from env_provider import DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, DB_SCHEMA_UPDATED, ENVIRONMENT
|
from env_provider import DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, DB_SCHEMA_UPDATED, ENVIRONMENT
|
||||||
|
|
||||||
engine = create_engine(f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}")
|
engine = create_engine(f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}")
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
|||||||
25
db/models/APIKey.py
Normal file
25
db/models/APIKey.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from datetime import datetime, timedelta, UTC
|
||||||
|
from sqlalchemy import Column, String, DateTime, Boolean, JSON
|
||||||
|
from db.models import Base
|
||||||
|
|
||||||
|
class APIKey(Base):
|
||||||
|
__tablename__ = 'apikey'
|
||||||
|
|
||||||
|
key = Column(String(64), primary_key=True)
|
||||||
|
name = Column(String(255), nullable=False)
|
||||||
|
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||||
|
last_used_at = Column(DateTime)
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
roles = Column(JSON, nullable=False, default=list)
|
||||||
|
expire = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC) + timedelta(days=15))
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"key": self.key,
|
||||||
|
"name": self.name,
|
||||||
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
|
"last_used_at": self.last_used_at.isoformat() if self.last_used_at else None,
|
||||||
|
"is_active": self.is_active,
|
||||||
|
"roles": self.roles,
|
||||||
|
"expire": self.expire.isoformat() if self.expire else None
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
#db/models/Log.py
|
|
||||||
from sqlalchemy import Column, Integer, String, DateTime, Text
|
from sqlalchemy import Column, Integer, String, DateTime, Text
|
||||||
from db.models import Base
|
from db.models import Base
|
||||||
import datetime
|
import datetime
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ class Markdown(Base):
|
|||||||
}
|
}
|
||||||
__payload__ = {
|
__payload__ = {
|
||||||
'dev': [
|
'dev': [
|
||||||
{'id': 1, 'title': 'index', 'content': ' ', 'created_at': datetime.datetime.utcnow, 'path_id': 1 },
|
{'id': 1, 'title': 'index', 'content': '{"markdown": ""}', 'created_at': datetime.datetime.utcnow, 'path_id': 1 },
|
||||||
],
|
],
|
||||||
'prod': [
|
'prod': [
|
||||||
{'id': 1, 'title': 'index', 'content': ' ', 'created_at': datetime.datetime.utcnow, 'path_id': 1},
|
{'id': 1, 'title': 'index', 'content': '{"markdown": ""}', 'created_at': datetime.datetime.utcnow, 'path_id': 1},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
15
db/models/MarkdownPermissionSetting.py
Normal file
15
db/models/MarkdownPermissionSetting.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from sqlalchemy import Column, Integer, ForeignKey, String
|
||||||
|
|
||||||
|
from db.models import Base
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownPermissionSetting(Base):
|
||||||
|
__tablename__ = 'markdown_permission_setting'
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
permission = Column(String(255), nullable=True)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"permission": self.permission,
|
||||||
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
from sqlalchemy import Column, Integer, ForeignKey
|
from sqlalchemy import Column, Integer, ForeignKey
|
||||||
|
|
||||||
from db.models import Base
|
from db.models import Base
|
||||||
|
|
||||||
|
|
||||||
class MarkdownSetting(Base):
|
class MarkdownSetting(Base):
|
||||||
__tablename__ = 'markdown_setting'
|
__tablename__ = 'markdown_setting'
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
template_setting_id = Column(Integer, ForeignKey('markdown_template.id'), nullable=True)
|
template_setting_id = Column(Integer, ForeignKey('markdown_template_setting.id'), nullable=True)
|
||||||
|
permission_setting_id = Column(Integer, ForeignKey('markdown_permission_setting.id'), nullable=True)
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"template_setting_id": self.template_setting_id,
|
"template_setting_id": self.template_setting_id,
|
||||||
|
"permission_setting_id": self.permission_setting_id,
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
from sqlalchemy import Column, Text, Integer, String, UniqueConstraint
|
from sqlalchemy import Column, Text, Integer, String, UniqueConstraint,JSON
|
||||||
from db.models import Base
|
from db.models import Base
|
||||||
|
|
||||||
class MarkdownTemplate(Base):
|
class MarkdownTemplate(Base):
|
||||||
__tablename__ = 'markdown_template'
|
__tablename__ = 'markdown_template'
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
title = Column(String(255), nullable=False)
|
title = Column(String(255), nullable=False)
|
||||||
parameters = Column(Text, nullable=True)
|
parameters = Column(JSON, nullable=True)
|
||||||
define = Column(Text, nullable=True)
|
layout = Column(Text, nullable=True)
|
||||||
__table_args__ = (UniqueConstraint("title", name="unique_title"),)
|
__table_args__ = (UniqueConstraint("title", name="unique_title"),)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
@@ -14,5 +14,5 @@ class MarkdownTemplate(Base):
|
|||||||
'id': self.id,
|
'id': self.id,
|
||||||
'title': self.title,
|
'title': self.title,
|
||||||
'parameters': self.parameters,
|
'parameters': self.parameters,
|
||||||
'define': self.define,
|
'layout': self.layout,
|
||||||
}
|
}
|
||||||
|
|||||||
16
db/models/MarkdownTemplateSetting.py
Normal file
16
db/models/MarkdownTemplateSetting.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from sqlalchemy import Column, Integer, ForeignKey
|
||||||
|
|
||||||
|
from db.models import Base
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownTemplateSetting(Base):
|
||||||
|
__tablename__ = 'markdown_template_setting'
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
template_id = Column(Integer, ForeignKey('markdown_template.id'))
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'template_id': self.template_id,
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ class Path(Base):
|
|||||||
name = Column(String(50), nullable=False)
|
name = Column(String(50), nullable=False)
|
||||||
parent_id = Column(Integer, ForeignKey("path.id"), nullable=True)
|
parent_id = Column(Integer, ForeignKey("path.id"), nullable=True)
|
||||||
order = Column(String(36), default=lambda: str(uuid.uuid4()))
|
order = Column(String(36), default=lambda: str(uuid.uuid4()))
|
||||||
setting_id = Column(Integer, ForeignKey("path_settings.id"), nullable=True)
|
setting_id = Column(Integer, ForeignKey("path_setting.id"), nullable=True)
|
||||||
__table_args__ = (UniqueConstraint("parent_id", "name", name="unique_parent_id_name"), )
|
__table_args__ = (UniqueConstraint("parent_id", "name", name="unique_parent_id_name"), )
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from db.models import Base
|
|||||||
|
|
||||||
|
|
||||||
class PathSetting(Base):
|
class PathSetting(Base):
|
||||||
__tablename__ = 'path_settings'
|
__tablename__ = 'path_setting'
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
webhook_setting_id = Column(Integer, ForeignKey('webhook_setting.id'), nullable=True)
|
webhook_setting_id = Column(Integer, ForeignKey('webhook_setting.id'), nullable=True)
|
||||||
template_setting_id = Column(Integer, ForeignKey('path_template.id'), nullable=True)
|
template_setting_id = Column(Integer, ForeignKey('path_template.id'), nullable=True)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#env_provider.py
|
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from db.models.Path import Path
|
from db.models.Path import Path
|
||||||
|
from db.models.PathSetting import PathSetting
|
||||||
from db.models.Webhook import Webhook
|
from db.models.Webhook import Webhook
|
||||||
from db.models.WebhookSetting import WebhookSetting
|
from db.models.WebhookSetting import WebhookSetting
|
||||||
|
from events import MARKDOWN_CREATED_EVENT, MARKDOWN_UPDATED_EVENT, MARKDOWN_DELETED_EVENT, PATH_CREATED_EVENT, \
|
||||||
|
PATH_UPDATED_EVENT, PATH_DELETED_EVENT
|
||||||
import abc
|
import abc
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
@@ -9,6 +12,15 @@ import pkgutil
|
|||||||
import requests
|
import requests
|
||||||
import db
|
import db
|
||||||
|
|
||||||
|
|
||||||
|
event_type_map = {
|
||||||
|
MARKDOWN_CREATED_EVENT: 'markdown_created_event',
|
||||||
|
MARKDOWN_UPDATED_EVENT: 'markdown_updated_event',
|
||||||
|
MARKDOWN_DELETED_EVENT: 'markdown_deleted_event',
|
||||||
|
PATH_CREATED_EVENT: 'path_created_event',
|
||||||
|
PATH_UPDATED_EVENT: 'path_updated_event',
|
||||||
|
PATH_DELETED_EVENT: 'path_deleted_event',
|
||||||
|
}
|
||||||
class WebhookEventHandler(abc.ABC):
|
class WebhookEventHandler(abc.ABC):
|
||||||
def __init__(self, event_type=0):
|
def __init__(self, event_type=0):
|
||||||
self.event_type = event_type
|
self.event_type = event_type
|
||||||
@@ -17,20 +29,21 @@ class WebhookEventHandler(abc.ABC):
|
|||||||
def get_path_id(self, payload):
|
def get_path_id(self, payload):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __call__(self, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
payload = kwargs['payload']
|
payload = kwargs['payload']
|
||||||
path_id = self.get_path_id(payload)
|
path_id = self.get_path_id(payload)
|
||||||
with db.get_db() as session:
|
with db.get_db() as session:
|
||||||
setting = self.get_setting(session, path_id)
|
setting = self.get_setting(session, path_id)
|
||||||
if setting is None:
|
if setting is None:
|
||||||
return
|
return
|
||||||
headers = {'Content-Type': 'application/json'}
|
headers = {'Content-Type': 'application/json', 'x-alchegos-event': event_type_map[self.event_type]}
|
||||||
if setting["additional_headers"] is not None:
|
if setting.get("additional_header", None) is not None:
|
||||||
headers.update(json.loads(setting["additional_headers"]))
|
headers.update(json.loads(setting["additional_header"]))
|
||||||
|
body = json.dumps(payload, default=str)
|
||||||
try:
|
try:
|
||||||
response = requests.post(setting["webhook_url"], json=payload, headers=headers, timeout=5)
|
response = requests.post(setting["webhook_url"], data=body, headers=headers, timeout=5)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.RequestException as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
def get_setting(self, session: Session, path_id):
|
def get_setting(self, session: Session, path_id):
|
||||||
@@ -39,15 +52,22 @@ class WebhookEventHandler(abc.ABC):
|
|||||||
if path is None:
|
if path is None:
|
||||||
return None
|
return None
|
||||||
p = path.to_dict()
|
p = path.to_dict()
|
||||||
webhook_setting = session.query(WebhookSetting).filter(WebhookSetting.path_id == path_id).first()
|
path_setting = session.query(PathSetting).get(path.setting_id)
|
||||||
|
if path_setting is None:
|
||||||
|
return None
|
||||||
|
webhook_setting = session.query(WebhookSetting).get(path_setting.webhook_setting_id)
|
||||||
|
|
||||||
if webhook_setting is None and p["parent_id"] != 1:
|
if webhook_setting is None and p["parent_id"] != 1:
|
||||||
return self.get_setting(session, p["parent_id"])
|
return self.get_setting(session, p["parent_id"])
|
||||||
|
|
||||||
|
# Check if webhook_setting is still None (e.g., when parent_id == 1 or no parent found)
|
||||||
|
if webhook_setting is None:
|
||||||
|
return None
|
||||||
|
|
||||||
setting = webhook_setting.to_dict()
|
setting = webhook_setting.to_dict()
|
||||||
if not setting["enabled"] or setting["on_events"] & self.event_type == 0:
|
if not setting["enabled"] or setting["on_events"] & self.event_type == 0:
|
||||||
return None
|
return None
|
||||||
webhook = session.query(Webhook).filter(Webhook.id == webhook_setting.webhook_id).first()
|
webhook = session.query(Webhook).get(webhook_setting.webhook_id)
|
||||||
if webhook is None:
|
if webhook is None:
|
||||||
return None
|
return None
|
||||||
setting["webhook_url"] = webhook.to_dict()["hook_url"]
|
setting["webhook_url"] = webhook.to_dict()["hook_url"]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#logging_handlers/DatabaseLogHandler.py
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from db import get_db
|
from db import get_db
|
||||||
from db.models.Log import Log
|
from db.models.Log import Log
|
||||||
|
|||||||
172
misc/backup_converters/__init__.py
Normal file
172
misc/backup_converters/__init__.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"""
|
||||||
|
Backup Conversion System
|
||||||
|
========================
|
||||||
|
|
||||||
|
This module provides functionality for converting backups between different versions.
|
||||||
|
Each version converter is implemented in a separate file and registered with the system.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
------
|
||||||
|
To convert a backup from one version to another:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from misc.backup_converters import convert_backup
|
||||||
|
|
||||||
|
# Convert a backup from version 1.0 to the latest version
|
||||||
|
convert_backup(backup_dir, target_version=None) # None means latest version
|
||||||
|
```
|
||||||
|
|
||||||
|
Adding a New Converter:
|
||||||
|
----------------------
|
||||||
|
1. Create a new file in the backup_converters directory, e.g., v1_to_v2.py
|
||||||
|
2. Implement a function that takes a backup directory and converts it to the target version
|
||||||
|
3. Register the converter in this file using the register_converter function
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
# In v1_to_v2.py
|
||||||
|
from misc.backup_converters import register_converter
|
||||||
|
|
||||||
|
def convert_v1_to_v2(backup_dir):
|
||||||
|
# Conversion logic here
|
||||||
|
return new_version
|
||||||
|
|
||||||
|
register_converter("1.0", "2.0", convert_v1_to_v2)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Callable, Tuple, Optional, List
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
CURRENT_VERSION = "1.0"
|
||||||
|
|
||||||
|
_converters: Dict[Tuple[str, str], Callable[[str], str]] = {}
|
||||||
|
|
||||||
|
def register_converter(from_version: str, to_version: str, converter: Callable[[str], str]):
|
||||||
|
"""
|
||||||
|
Register a converter function for a specific version transition.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
from_version (str): The source version
|
||||||
|
to_version (str): The target version
|
||||||
|
converter (callable): A function that takes a backup directory path and returns the new version
|
||||||
|
"""
|
||||||
|
_converters[(from_version, to_version)] = converter
|
||||||
|
logger.info(f"Registered converter from version {from_version} to {to_version}")
|
||||||
|
|
||||||
|
def get_backup_version(backup_dir: str) -> str:
|
||||||
|
"""
|
||||||
|
Get the version of a backup.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backup_dir (str): Path to the backup directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The version of the backup, or "0.0" if no version is found
|
||||||
|
"""
|
||||||
|
version_file = os.path.join(backup_dir, "version.json")
|
||||||
|
if os.path.exists(version_file):
|
||||||
|
try:
|
||||||
|
with open(version_file, 'r') as f:
|
||||||
|
version_data = json.load(f)
|
||||||
|
return version_data.get("version", "0.0")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading version file: {e}")
|
||||||
|
return "0.0"
|
||||||
|
|
||||||
|
def set_backup_version(backup_dir: str, version: str):
|
||||||
|
"""
|
||||||
|
Set the version of a backup.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backup_dir (str): Path to the backup directory
|
||||||
|
version (str): The version to set
|
||||||
|
"""
|
||||||
|
version_file = os.path.join(backup_dir, "version.json")
|
||||||
|
try:
|
||||||
|
with open(version_file, 'w') as f:
|
||||||
|
json.dump({"version": version}, f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error writing version file: {e}")
|
||||||
|
|
||||||
|
def find_conversion_path(from_version: str, to_version: str) -> Optional[List[Tuple[str, str]]]:
|
||||||
|
"""
|
||||||
|
Find a path of converters to go from one version to another.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
from_version (str): The source version
|
||||||
|
to_version (str): The target version
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of (from, to) version pairs representing the conversion path,
|
||||||
|
or None if no path is found
|
||||||
|
"""
|
||||||
|
if from_version == to_version:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if (from_version, to_version) in _converters:
|
||||||
|
return [(from_version, to_version)]
|
||||||
|
|
||||||
|
queue = [(from_version, [])]
|
||||||
|
visited = {from_version}
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
current, path = queue.pop(0)
|
||||||
|
|
||||||
|
for (src, dst), _ in _converters.items():
|
||||||
|
if src == current and dst not in visited:
|
||||||
|
new_path = path + [(src, dst)]
|
||||||
|
if dst == to_version:
|
||||||
|
return new_path
|
||||||
|
visited.add(dst)
|
||||||
|
queue.append((dst, new_path))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def convert_backup(backup_dir: str, target_version: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Convert a backup to the target version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backup_dir (str): Path to the backup directory
|
||||||
|
target_version (str, optional): The target version. If None, converts to the latest version.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The new version of the backup
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no conversion path is found
|
||||||
|
"""
|
||||||
|
if target_version is None:
|
||||||
|
target_version = CURRENT_VERSION
|
||||||
|
|
||||||
|
current_version = get_backup_version(backup_dir)
|
||||||
|
if current_version == target_version:
|
||||||
|
return current_version
|
||||||
|
|
||||||
|
path = find_conversion_path(current_version, target_version)
|
||||||
|
if not path:
|
||||||
|
raise ValueError(f"No conversion path found from version {current_version} to {target_version}")
|
||||||
|
|
||||||
|
for from_ver, to_ver in path:
|
||||||
|
converter = _converters.get((from_ver, to_ver))
|
||||||
|
if converter:
|
||||||
|
logger.info(f"Converting backup from version {from_ver} to {to_ver}")
|
||||||
|
new_version = converter(backup_dir)
|
||||||
|
set_backup_version(backup_dir, new_version)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Converter not found for {from_ver} to {to_ver}")
|
||||||
|
|
||||||
|
return target_version
|
||||||
|
|
||||||
|
import pkgutil
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
for _, name, is_pkg in pkgutil.iter_modules([os.path.dirname(__file__)]):
|
||||||
|
if not is_pkg and name != "__init__":
|
||||||
|
importlib.import_module(f"misc.backup_converters.{name}")
|
||||||
176
misc/backup_converters/v0_to_v1.py
Normal file
176
misc/backup_converters/v0_to_v1.py
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
"""
|
||||||
|
Converter from version 0.0 to version 1.0
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
This module converts backups from version 0.0 (old format without version info)
|
||||||
|
to version 1.0 (current format with version info).
|
||||||
|
|
||||||
|
The old version backup has a simple directory structure with .md files for markdown content,
|
||||||
|
.mdmeta files for markdown metadata, and .meta files for path metadata.
|
||||||
|
|
||||||
|
The current version backup has a more complex structure with tree/ and template/ directories,
|
||||||
|
.json files for markdown content, .json.meta files for path metadata, and a webhook.json file.
|
||||||
|
|
||||||
|
For all old version markdowns, they are considered to be using the default template with no other settings,
|
||||||
|
and their content is converted from plain text to {"markdown": content_in_md}.
|
||||||
|
|
||||||
|
For all old version paths, they are considered to have no settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
from misc.backup_converters import register_converter
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def convert_v0_to_v1(backup_dir: str) -> str:
|
||||||
|
"""
|
||||||
|
Convert a backup from version 0.0 to version 1.0.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backup_dir (str): Path to the backup directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The new version ("1.0")
|
||||||
|
"""
|
||||||
|
logger.info(f"Converting backup from version 0.0 to 1.0: {backup_dir}")
|
||||||
|
|
||||||
|
temp_dir = tempfile.mkdtemp()
|
||||||
|
new_backup_dir = os.path.join(temp_dir, "new_backup")
|
||||||
|
os.makedirs(new_backup_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree_dir = os.path.join(new_backup_dir, 'tree')
|
||||||
|
template_dir = os.path.join(new_backup_dir, 'template')
|
||||||
|
os.makedirs(tree_dir)
|
||||||
|
os.makedirs(template_dir)
|
||||||
|
|
||||||
|
with open(os.path.join(new_backup_dir, 'webhook.json'), 'w') as f:
|
||||||
|
json.dump([], f)
|
||||||
|
|
||||||
|
with open(os.path.join(new_backup_dir, 'version.json'), 'w') as f:
|
||||||
|
json.dump({"version": "1.0"}, f)
|
||||||
|
|
||||||
|
root_dir = os.path.join(backup_dir, 'Root')
|
||||||
|
if os.path.exists(root_dir) and os.path.isdir(root_dir):
|
||||||
|
source_dir = root_dir
|
||||||
|
else:
|
||||||
|
source_dir = backup_dir
|
||||||
|
|
||||||
|
convert_directory(source_dir, tree_dir)
|
||||||
|
|
||||||
|
for item in os.listdir(new_backup_dir):
|
||||||
|
src = os.path.join(new_backup_dir, item)
|
||||||
|
dst = os.path.join(backup_dir, item)
|
||||||
|
if os.path.isdir(src):
|
||||||
|
if os.path.exists(dst):
|
||||||
|
shutil.rmtree(dst)
|
||||||
|
shutil.copytree(src, dst)
|
||||||
|
else:
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
|
||||||
|
return "1.0"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error converting backup: {e}")
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def convert_directory(old_dir: str, new_dir: str):
|
||||||
|
"""
|
||||||
|
Recursively convert a directory from the old backup format to the new format.
|
||||||
|
|
||||||
|
This function processes a directory from the old backup format and converts it to the new format.
|
||||||
|
It creates a .json.meta file for the directory with metadata from the .meta file (if it exists),
|
||||||
|
processes all markdown files (.md) in the directory, converting them to .json files with the new format,
|
||||||
|
and recursively processes all subdirectories.
|
||||||
|
|
||||||
|
For markdown files, the function:
|
||||||
|
1. Reads the content from the .md file
|
||||||
|
2. Converts the content to the new format {"markdown": content_in_md}
|
||||||
|
3. Reads metadata from the .mdmeta file (if it exists)
|
||||||
|
4. Creates a new .json file with the content and metadata
|
||||||
|
|
||||||
|
For directories, the function:
|
||||||
|
1. Reads metadata from the .meta file (if it exists)
|
||||||
|
2. Creates a new .json.meta file with the metadata
|
||||||
|
3. Recursively processes all subdirectories
|
||||||
|
|
||||||
|
Args:
|
||||||
|
old_dir (str): Path to the old directory
|
||||||
|
new_dir (str): Path to the new directory where the converted files will be stored
|
||||||
|
"""
|
||||||
|
order = ''
|
||||||
|
meta_file_path = os.path.join(old_dir, '.meta')
|
||||||
|
if os.path.exists(meta_file_path):
|
||||||
|
with open(meta_file_path, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
if line.startswith('order:'):
|
||||||
|
order = line.strip().split(': ', 1)[1]
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
path_meta = {
|
||||||
|
'name': os.path.basename(old_dir),
|
||||||
|
'order': order,
|
||||||
|
'backup_id': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(os.path.join(new_dir, '.json.meta'), 'w') as f:
|
||||||
|
json.dump(path_meta, f, default=str, indent=2)
|
||||||
|
|
||||||
|
for file_name in os.listdir(old_dir):
|
||||||
|
old_file_path = os.path.join(old_dir, file_name)
|
||||||
|
|
||||||
|
if file_name.endswith('.md'):
|
||||||
|
md_title = file_name[:-3]
|
||||||
|
|
||||||
|
with open(old_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
new_content = json.dumps({'markdown': content})
|
||||||
|
|
||||||
|
created_at = datetime.now()
|
||||||
|
order = ''
|
||||||
|
shortcut = ''
|
||||||
|
|
||||||
|
mdmeta_path = old_file_path + 'meta'
|
||||||
|
if os.path.exists(mdmeta_path):
|
||||||
|
with open(mdmeta_path, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
key, value = line.strip().split(': ', 1)
|
||||||
|
if key == 'created_at':
|
||||||
|
created_at = value
|
||||||
|
elif key == 'order':
|
||||||
|
order = value
|
||||||
|
elif key == 'shortcut':
|
||||||
|
shortcut = value
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
md_data = {
|
||||||
|
'title': md_title,
|
||||||
|
'content': new_content,
|
||||||
|
'created_at': created_at,
|
||||||
|
'order': order,
|
||||||
|
'shortcut': shortcut,
|
||||||
|
'backup_id': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(os.path.join(new_dir, f'{md_title}.json'), 'w') as f:
|
||||||
|
json.dump(md_data, f, default=str, indent=2)
|
||||||
|
|
||||||
|
elif os.path.isdir(old_file_path) and not file_name.startswith('.'):
|
||||||
|
new_subdir = os.path.join(new_dir, file_name)
|
||||||
|
os.makedirs(new_subdir, exist_ok=True)
|
||||||
|
convert_directory(old_file_path, new_subdir)
|
||||||
|
|
||||||
|
register_converter("0.0", "1.0", convert_v0_to_v1)
|
||||||
9
req2.txt
9
req2.txt
@@ -1,9 +0,0 @@
|
|||||||
cryptography==44.0.0
|
|
||||||
Flask==3.1.0
|
|
||||||
Flask_Cors==5.0.0
|
|
||||||
Flask_Limiter==3.9.2
|
|
||||||
PyJWT==2.10.1
|
|
||||||
PyJWT==2.10.1
|
|
||||||
python-dotenv==1.0.1
|
|
||||||
Requests==2.32.3
|
|
||||||
SQLAlchemy==2.0.36
|
|
||||||
5
requirements-test.txt
Normal file
5
requirements-test.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Test dependencies
|
||||||
|
pytest==7.4.0
|
||||||
|
pytest-flask==1.2.0
|
||||||
|
pytest-cov==4.1.0
|
||||||
|
pytest-mock==3.11.1
|
||||||
@@ -34,3 +34,5 @@ typing_extensions==4.12.2
|
|||||||
urllib3==2.2.3
|
urllib3==2.2.3
|
||||||
Werkzeug==3.1.3
|
Werkzeug==3.1.3
|
||||||
wrapt==1.17.0
|
wrapt==1.17.0
|
||||||
|
|
||||||
|
pytest~=8.3.5
|
||||||
Reference in New Issue
Block a user