This commit is contained in:
Piero Toffanin 2022-12-31 17:34:26 -05:00
commit 67453b0c15
6 changed files with 63 additions and 36 deletions

View file

@ -1 +1 @@
1.3.7 1.3.8

View file

@ -8,8 +8,8 @@ from timeit import default_timer
import argostranslatefiles import argostranslatefiles
from argostranslatefiles import get_supported_formats from argostranslatefiles import get_supported_formats
from flask import (Flask, abort, jsonify, render_template, request, send_file, from flask import (abort, Blueprint, Flask, jsonify, render_template, request,
url_for, Response) Response, send_file, url_for)
from flask_swagger import swagger from flask_swagger import swagger
from flask_swagger_ui import get_swaggerui_blueprint from flask_swagger_ui import get_swaggerui_blueprint
from translatehtml import translate_html from translatehtml import translate_html
@ -106,10 +106,10 @@ def create_app(args):
from libretranslate.language import load_languages from libretranslate.language import load_languages
app = Flask(__name__) SWAGGER_URL = args.url_prefix + "/docs" # Swagger UI (w/o trailing '/')
API_URL = args.url_prefix + "/spec"
if args.debug: bp = Blueprint('Main app', __name__)
app.config["TEMPLATES_AUTO_RELOAD"] = True
if not args.disable_files_translation: if not args.disable_files_translation:
remove_translated_files.setup(get_upload_dir()) remove_translated_files.setup(get_upload_dir())
@ -161,7 +161,7 @@ def create_app(args):
from flask_limiter import Limiter from flask_limiter import Limiter
limiter = Limiter( limiter = Limiter(
app, bp,
key_func=get_remote_address, key_func=get_remote_address,
default_limits=get_routes_limits( default_limits=get_routes_limits(
args.req_limit, args.daily_req_limit, api_keys_db args.req_limit, args.daily_req_limit, api_keys_db
@ -181,7 +181,7 @@ def create_app(args):
if args.metrics: if args.metrics:
from prometheus_client import CONTENT_TYPE_LATEST, Summary, Gauge, CollectorRegistry, multiprocess, generate_latest from prometheus_client import CONTENT_TYPE_LATEST, Summary, Gauge, CollectorRegistry, multiprocess, generate_latest
@app.route("/metrics") @bp.route("/metrics")
@limiter.exempt @limiter.exempt
def prometheus_metrics(): def prometheus_metrics():
if args.metrics_auth_token: if args.metrics_auth_token:
@ -252,24 +252,24 @@ def create_app(args):
else: else:
return func return func
@app.errorhandler(400) @bp.errorhandler(400)
def invalid_api(e): def invalid_api(e):
return jsonify({"error": str(e.description)}), 400 return jsonify({"error": str(e.description)}), 400
@app.errorhandler(500) @bp.errorhandler(500)
def server_error(e): def server_error(e):
return jsonify({"error": str(e.description)}), 500 return jsonify({"error": str(e.description)}), 500
@app.errorhandler(429) @bp.errorhandler(429)
def slow_down_error(e): def slow_down_error(e):
flood.report(get_remote_address()) flood.report(get_remote_address())
return jsonify({"error": "Slowdown: " + str(e.description)}), 429 return jsonify({"error": "Slowdown: " + str(e.description)}), 429
@app.errorhandler(403) @bp.errorhandler(403)
def denied(e): def denied(e):
return jsonify({"error": str(e.description)}), 403 return jsonify({"error": str(e.description)}), 403
@app.route("/") @bp.route("/")
@limiter.exempt @limiter.exempt
def index(): def index():
if args.disable_web_ui: if args.disable_web_ui:
@ -282,10 +282,12 @@ def create_app(args):
api_keys=args.api_keys, api_keys=args.api_keys,
get_api_key_link=args.get_api_key_link, get_api_key_link=args.get_api_key_link,
web_version=os.environ.get("LT_WEB") is not None, web_version=os.environ.get("LT_WEB") is not None,
version=get_version() version=get_version(),
swagger_url=SWAGGER_URL,
url_prefix=args.url_prefix
) )
@app.get("/javascript-licenses") @bp.get("/javascript-licenses")
@limiter.exempt @limiter.exempt
def javascript_licenses(): def javascript_licenses():
if args.disable_web_ui: if args.disable_web_ui:
@ -293,7 +295,7 @@ def create_app(args):
return render_template("javascript-licenses.html") return render_template("javascript-licenses.html")
@app.get("/languages") @bp.get("/languages")
@limiter.exempt @limiter.exempt
def langs(): def langs():
""" """
@ -325,7 +327,7 @@ def create_app(args):
return jsonify([{"code": l.code, "name": l.name, "targets": language_pairs.get(l.code, [])} for l in languages]) return jsonify([{"code": l.code, "name": l.name, "targets": language_pairs.get(l.code, [])} for l in languages])
# Add cors # Add cors
@app.after_request @bp.after_request
def after_request(response): def after_request(response):
response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add( response.headers.add(
@ -337,7 +339,7 @@ def create_app(args):
response.headers.add("Access-Control-Max-Age", 60 * 60 * 24 * 20) response.headers.add("Access-Control-Max-Age", 60 * 60 * 24 * 20)
return response return response
@app.post("/translate") @bp.post("/translate")
@access_check @access_check
def translate(): def translate():
""" """
@ -577,7 +579,7 @@ def create_app(args):
except Exception as e: except Exception as e:
abort(500, description="Cannot translate text: %s" % str(e)) abort(500, description="Cannot translate text: %s" % str(e))
@app.post("/translate_file") @bp.post("/translate_file")
@access_check @access_check
def translate_file(): def translate_file():
""" """
@ -710,7 +712,7 @@ def create_app(args):
except Exception as e: except Exception as e:
abort(500, description=e) abort(500, description=e)
@app.get("/download_file/<string:filename>") @bp.get("/download_file/<string:filename>")
def download_file(filename: str): def download_file(filename: str):
""" """
Download a translated file Download a translated file
@ -737,7 +739,7 @@ def create_app(args):
return send_file(return_data, as_attachment=True, download_name=download_filename) return send_file(return_data, as_attachment=True, download_name=download_filename)
@app.post("/detect") @bp.post("/detect")
@access_check @access_check
def detect(): def detect():
""" """
@ -831,7 +833,7 @@ def create_app(args):
return jsonify(detect_languages(q)) return jsonify(detect_languages(q))
@app.route("/frontend/settings") @bp.route("/frontend/settings")
@limiter.exempt @limiter.exempt
def frontend_settings(): def frontend_settings():
""" """
@ -910,7 +912,7 @@ def create_app(args):
} }
) )
@app.post("/suggest") @bp.post("/suggest")
@access_check @access_check
def suggest(): def suggest():
""" """
@ -987,21 +989,29 @@ def create_app(args):
SuggestionsDatabase().add(q, s, source_lang, target_lang) SuggestionsDatabase().add(q, s, source_lang, target_lang)
return jsonify({"success": True}) return jsonify({"success": True})
app = Flask(__name__)
if args.debug:
app.config["TEMPLATES_AUTO_RELOAD"] = True
if args.url_prefix:
app.register_blueprint(bp, url_prefix=args.url_prefix)
else:
app.register_blueprint(bp)
swag = swagger(app) swag = swagger(app)
swag["info"]["version"] = "1.3.1" swag["info"]["version"] = get_version()
swag["info"]["title"] = "LibreTranslate" swag["info"]["title"] = "LibreTranslate"
@app.route("/spec")
@app.route(API_URL)
@limiter.exempt @limiter.exempt
def spec(): def spec():
return jsonify(swag) return jsonify(swag)
SWAGGER_URL = "/docs" # URL for exposing Swagger UI (without trailing '/')
API_URL = "/spec"
# Call factory function to create our blueprint # Call factory function to create our blueprint
swaggerui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL) swaggerui_blueprint = get_swaggerui_blueprint(SWAGGER_URL, API_URL)
if args.url_prefix:
app.register_blueprint(swaggerui_blueprint) app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL)
else:
app.register_blueprint(swaggerui_blueprint)
return app return app

View file

@ -171,6 +171,11 @@ _default_options_objects = [
'default_value': '', 'default_value': '',
'value_type': 'str' 'value_type': 'str'
}, },
{
'name': 'URL_PREFIX',
'default_value': '',
'value_type': 'str'
},
] ]

View file

@ -159,14 +159,23 @@ def get_args():
type=str, type=str,
help="Protect the /metrics endpoint by allowing only clients that have a valid Authorization Bearer token (%(default)s)", help="Protect the /metrics endpoint by allowing only clients that have a valid Authorization Bearer token (%(default)s)",
) )
return parser.parse_args() parser.add_argument(
"--url-prefix",
default=DEFARGS['URL_PREFIX'],
type=str,
help="Add prefix to URL: example.com:5000/url-prefix/",
)
args = parser.parse_args()
if args.url_prefix and not args.url_prefix.startswith('/'):
args.url_prefix = '/' + args.url_prefix
return args
def main(): def main():
args = get_args() args = get_args()
app = create_app(args) app = create_app(args)
if sys.argv[0] == '--wsgi': if '--wsgi' in sys.argv:
return app return app
else: else:
if args.debug: if args.debug:
@ -175,7 +184,7 @@ def main():
from waitress import serve from waitress import serve
url_scheme = "https" if args.ssl else "http" url_scheme = "https" if args.ssl else "http"
print("Running on %s://%s:%s" % (url_scheme, args.host, args.port)) print("Running on %s://%s:%s%s" % (url_scheme, args.host, args.port, args.url_prefix))
serve( serve(
app, app,

View file

@ -1,6 +1,6 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
// API host/endpoint // API host/endpoint
var BaseUrl = window.location.protocol + "//" + window.location.host; var BaseUrl = window.location.protocol + "//" + window.location.host + url_prefix ;
var htmlRegex = /<(.*)>.*?|<(.*)\/>/; var htmlRegex = /<(.*)>.*?|<(.*)\/>/;
document.addEventListener('DOMContentLoaded', function(){ document.addEventListener('DOMContentLoaded', function(){
var sidenavElems = document.querySelectorAll('.sidenav'); var sidenavElems = document.querySelectorAll('.sidenav');

View file

@ -7,6 +7,9 @@
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<meta name="description" content="Free and Open Source Machine Translation API. 100% self-hosted, offline capable and easy to setup. Run your own API server in just a few minutes."> <meta name="description" content="Free and Open Source Machine Translation API. 100% self-hosted, offline capable and easy to setup. Run your own API server in just a few minutes.">
<meta name="keywords" content="translation,api"> <meta name="keywords" content="translation,api">
<script type="text/javascript">
var url_prefix = "{{ url_prefix }}"
</script>
<link rel="preload" href="{{ url_for('static', filename='icon.svg') }}" as="image" /> <link rel="preload" href="{{ url_for('static', filename='icon.svg') }}" as="image" />
<link rel="preload" href="{{ url_for('static', filename='js/vue@2.js') }}" as="script"> <link rel="preload" href="{{ url_for('static', filename='js/vue@2.js') }}" as="script">
@ -58,7 +61,7 @@
<span>LibreTranslate</span> <span>LibreTranslate</span>
</a> </a>
<ul class="right hide-on-med-and-down"> <ul class="right hide-on-med-and-down">
<li><a href="/docs">API Docs</a></li> <li><a href="{{ swagger_url }}">API Docs</a></li>
{% if get_api_key_link %} {% if get_api_key_link %}
<li><a href="{{ get_api_key_link }}">Get API Key</a></li> <li><a href="{{ get_api_key_link }}">Get API Key</a></li>
<script>window.getApiKeyLink = "{{ get_api_key_link }}";</script> <script>window.getApiKeyLink = "{{ get_api_key_link }}";</script>
@ -70,7 +73,7 @@
</ul> </ul>
<ul id="nav-mobile" class="sidenav"> <ul id="nav-mobile" class="sidenav">
<li><a href="/docs">API Docs</a></li> <li><a href="{{ swagger_url }}">API Docs</a></li>
{% if get_api_key_link %} {% if get_api_key_link %}
<li><a href="{{ get_api_key_link }}">Get API Key</a></li> <li><a href="{{ get_api_key_link }}">Get API Key</a></li>
{% endif %} {% endif %}