Moved some scripts to a utils folder

Renamed upload route to file as its more approprete
Fixed random CSS issues that occur on older browsers or Safari
This commit is contained in:
Michał 2023-03-14 22:07:17 +00:00
parent 9cfb8befd2
commit 3008a55899
19 changed files with 102 additions and 76 deletions

View file

@ -61,7 +61,7 @@ Try checking if you have `XDG_CONFIG_HOME` setup. If you don't, you can set that
export XDG_CONFIG_HOME="$HOME/.config" export XDG_CONFIG_HOME="$HOME/.config"
## Finally notes ## Final notes
Thank you to everyone who helped me test the previous and current versions of the gallery, especially critters: Thank you to everyone who helped me test the previous and current versions of the gallery, especially critters:

View file

@ -13,6 +13,8 @@ from flask_caching import Cache
from flask_assets import Environment, Bundle from flask_assets import Environment, Bundle
from flask import Flask, render_template from flask import Flask, render_template
from gallery.utils import theme_manager
# Configuration # Configuration
from dotenv import load_dotenv from dotenv import load_dotenv
import platformdirs import platformdirs
@ -61,7 +63,6 @@ def create_app(test_config=None):
pass pass
# Load theme # Load theme
from . import theme_manager
theme_manager.CompileTheme('default', app.root_path) theme_manager.CompileTheme('default', app.root_path)
# Bundle JS files # Bundle JS files

View file

@ -58,6 +58,18 @@ class Posts (base): # pylint: disable=too-few-public-methods, C0103
post_alt = Column(String, nullable=False) post_alt = Column(String, nullable=False)
junction = relationship('GroupJunction', backref='posts') junction = relationship('GroupJunction', backref='posts')
class Thumbnails (base): # pylint: disable=too-few-public-methods, C0103
"""
Thumbnail table
"""
__tablename__ = 'thumbnails'
id = Column(Integer, primary_key=True)
file_name = Column(String, unique=True, nullable=False)
file_ext = Column(String, nullable=False)
data = Column(PickleType, nullable=False)
class Groups (base): # pylint: disable=too-few-public-methods, C0103 class Groups (base): # pylint: disable=too-few-public-methods, C0103

View file

@ -4,6 +4,7 @@ Used intermally by the frontend and possibly by other applications
""" """
from uuid import uuid4 from uuid import uuid4
import os import os
import pathlib
import io import io
import logging import logging
from datetime import datetime as dt from datetime import datetime as dt
@ -19,7 +20,7 @@ from sqlalchemy.orm import sessionmaker
from gallery.auth import login_required from gallery.auth import login_required
from gallery import db from gallery import db
from gallery import metadata as mt from gallery.utils import metadata as mt
blueprint = Blueprint('api', __name__, url_prefix='/api') blueprint = Blueprint('api', __name__, url_prefix='/api')
@ -27,58 +28,77 @@ db_session = sessionmaker(bind=db.engine)
db_session = db_session() db_session = db_session()
@blueprint.route('/uploads/<file>', methods=['GET']) @blueprint.route('/file/<file_name>', methods=['GET'])
def uploads(file): def get_file(file_name):
""" """
Returns a file from the uploads folder Returns a file from the uploads folder
t is the type of file (thumb, etc)
w and h are the width and height of the image for resizing w and h are the width and height of the image for resizing
f is whether to apply filters to the image, such as blurring NSFW images f is whether to apply filters to the image, such as blurring NSFW images
b is whether to force blur the image, even if it's not NSFW b is whether to force blur the image, even if it's not NSFW
""" """
# Get args # Get args
type = request.args.get('t', default=None, type=str) # Type of file (thumb, etc)
width = request.args.get('w', default=0, type=int) # Width of image width = request.args.get('w', default=0, type=int) # Width of image
height = request.args.get('h', default=0, type=int) # Height of image height = request.args.get('h', default=0, type=int) # Height of image
filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters
blur = request.args.get('b', default=False, type=bool) # Whether to force blur blur = request.args.get('b', default=False, type=bool) # Whether to force blur
# if no args are passed, return the raw file file_name = secure_filename(file_name) # Sanitize file name
if width == 0 and height == 0 and not filtered:
if not os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'], # If type is thumb(nail), return from database instead of file system
secure_filename(file))): # as it's faster than generating a new thumbnail on every request
if type == 'thumb':
thumb = db_session.query(db.Thumbnails).filter_by(file_name=file_name).first()
# If no thumbnail exists, return 404
if not thumb:
abort(404) abort(404)
return send_from_directory(current_app.config['UPLOAD_FOLDER'], file, as_attachment=True)
# Of either width or height is 0, set it to the other value to keep aspect ratio return send_file(thumb.data, mimetype='image/' + thumb.file_ext)
if width > 0 and height == 0:
# if no args are passed, return the raw file
if not request.args:
if not os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'], file_name)):
abort(404)
return send_from_directory(current_app.config['UPLOAD_FOLDER'], file_name)
# If only width is passed, set height to width
if width and not height:
height = width height = width
elif width == 0 and height > 0: # If only height is passed, set width to height
elif not width and height:
width = height width = height
# If neither are passed, return 400 as one is required for resizing
elif not width and not height:
abort(400)
buff = io.BytesIO() buff = io.BytesIO() # Image Buffer
# Open image and set extension # Open image and set extension
try: try:
img = Image.open(os.path.join(current_app.config['UPLOAD_FOLDER'], file)) img = Image.open(os.path.join(current_app.config['UPLOAD_FOLDER'], file_name))
# FileNotFound is raised if the file doesn't exist
except FileNotFoundError: except FileNotFoundError:
logging.error('File not found: %s, possibly broken upload', file) logging.error('File not found: %s', file_name)
abort(404) abort(404)
except Exception as err: # OSError is raised if the file is broken or corrupted
logging.error('Error opening image: %s', err) except OSError as err:
logging.error('Possibly broken image %s, error: %s', file_name, err)
abort(500) abort(500)
img_ext = os.path.splitext(file)[-1].lower().replace('.', '') img_ext = pathlib.Path(file_name).suffix.replace('.', '').lower() # Get file extension
img_ext = current_app.config['ALLOWED_EXTENSIONS'][img_ext] img_ext = current_app.config['ALLOWED_EXTENSIONS'][img_ext] # Convert to MIME type
img_icc = img.info.get("icc_profile") # Get ICC profile as it alters colours when saving img_icc = img.info.get("icc_profile") # Get ICC profile
# Resize image and orientate correctly img.thumbnail((width, height), Image.LANCZOS) # Resize image
img.thumbnail((width, height), Image.LANCZOS) img = ImageOps.exif_transpose(img) # Rotate image based on EXIF data
img = ImageOps.exif_transpose(img)
# If has NSFW tag, blur image, etc. # If has NSFW tag, blur image, etc.
if filtered: if filtered:
# img = img.filter(ImageFilter.GaussianBlur(20))
pass pass
# If forced to blur, blur image # If forced to blur, blur image
if blur: if blur:
img = img.filter(ImageFilter.GaussianBlur(20)) img = img.filter(ImageFilter.GaussianBlur(20))
@ -91,12 +111,12 @@ def uploads(file):
img = img.convert('RGB') img = img.convert('RGB')
img.save(buff, img_ext, icc_profile=img_icc) img.save(buff, img_ext, icc_profile=img_icc)
except Exception as err: except Exception as err:
logging.error('Could not resize image %s, error: %s', file, err) logging.error('Could not resize image %s, error: %s', file_name, err)
abort(500) abort(500)
img.close() img.close() # Close image to free memory, learned the hard way
buff.seek(0) # Reset buffer to start buff.seek(0) # Reset buffer to start
return send_file(buff, mimetype='image/' + img_ext) return send_file(buff, mimetype='image/' + img_ext)
@ -108,23 +128,21 @@ def upload():
""" """
form_file = request.files['file'] form_file = request.files['file']
form = request.form form = request.form
form_description = form['description']
form_alt = form['alt']
# If no image is uploaded, return 404 error
if not form_file: if not form_file:
return abort(404) return abort(404)
img_ext = os.path.splitext(form_file.filename)[-1].replace('.', '').lower() # Get file extension, generate random name and set file path
img_ext = pathlib.Path(form_file.filename).suffix.replace('.', '').lower()
img_name = "GWAGWA_"+str(uuid4()) img_name = "GWAGWA_"+str(uuid4())
img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img_name+'.'+img_ext) img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], img_name+'.'+img_ext)
# Check if file extension is allowed
if img_ext not in current_app.config['ALLOWED_EXTENSIONS'].keys(): if img_ext not in current_app.config['ALLOWED_EXTENSIONS'].keys():
logging.info('File extension not allowed: %s', img_ext) logging.info('File extension not allowed: %s', img_ext)
abort(403) abort(403)
if os.path.isdir(current_app.config['UPLOAD_FOLDER']) is False:
os.mkdir(current_app.config['UPLOAD_FOLDER'])
# Save file # Save file
try: try:
form_file.save(img_path) form_file.save(img_path)
@ -132,28 +150,27 @@ def upload():
logging.error('Could not save file: %s', err) logging.error('Could not save file: %s', err)
abort(500) abort(500)
# Get metadata and colors img_exif = mt.Metadata(img_path).yoink() # Get EXIF data
img_exif = mt.Metadata(img_path).yoink() img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
img_colors = ColorThief(img_path).get_palette(color_count=3)
# Save to database # Save to database
try: try:
query = db.Posts(author_id=g.user.id, query = db.Posts(author_id=g.user.id,
created_at=dt.utcnow(), created_at=dt.utcnow(),
file_name=img_name+'.'+img_ext, file_name=img_name+'.'+img_ext,
file_type=img_ext, file_type=img_ext,
image_exif=img_exif, image_exif=img_exif,
image_colours=img_colors, image_colours=img_colors,
post_description=form_description, post_description=form['description'],
post_alt=form_alt) post_alt=form['alt'])
db_session.add(query) db_session.add(query)
db_session.commit() db_session.commit()
except Exception as err: except Exception as err:
logging.error('Could not save to database: %s', err) logging.error('Could not save to database: %s', err)
abort(500) abort(500)
return 'Gwa Gwa' return 'Gwa Gwa' # Return something so the browser doesn't show an error
@blueprint.route('/delete/<int:image_id>', methods=['POST']) @blueprint.route('/delete/<int:image_id>', methods=['POST'])
@ -180,11 +197,11 @@ def delete_image(image_id):
try: try:
db_session.query(db.Posts).filter_by(id=image_id).delete() db_session.query(db.Posts).filter_by(id=image_id).delete()
groups = db_session.query(db.GroupJunction).filter_by(post_id=image_id).all() groups = db_session.query(db.GroupJunction).filter_by(post_id=image_id).all()
for group in groups: for group in groups:
db_session.delete(group) db_session.delete(group)
db_session.commit() db_session.commit()
except Exception as err: except Exception as err:
logging.error('Could not remove from database: %s', err) logging.error('Could not remove from database: %s', err)
@ -205,10 +222,10 @@ def create_group():
description=request.form['description'], description=request.form['description'],
author_id=g.user.id, author_id=g.user.id,
created_at=dt.utcnow()) created_at=dt.utcnow())
db_session.add(new_group) db_session.add(new_group)
db_session.commit() db_session.commit()
return ':3' return ':3'
@ -220,7 +237,7 @@ def modify_group():
""" """
group_id = request.form['group'] group_id = request.form['group']
image_id = request.form['image'] image_id = request.form['image']
group = db_session.query(db.Groups).filter_by(id=group_id).first() group = db_session.query(db.Groups).filter_by(id=group_id).first()
if group is None: if group is None:
@ -233,9 +250,9 @@ def modify_group():
db_session.add(db.GroupJunction(group_id=group_id, post_id=image_id, date_added=dt.utcnow())) db_session.add(db.GroupJunction(group_id=group_id, post_id=image_id, date_added=dt.utcnow()))
elif request.form['action'] == 'remove': elif request.form['action'] == 'remove':
db_session.query(db.GroupJunction).filter_by(group_id=group_id, post_id=image_id).delete() db_session.query(db.GroupJunction).filter_by(group_id=group_id, post_id=image_id).delete()
db_session.commit() db_session.commit()
return ':3' return ':3'
@ -262,10 +279,9 @@ def logfile():
Gets the log file and returns it as a JSON object Gets the log file and returns it as a JSON object
""" """
log_dict = {} log_dict = {}
i = 0
with open('only.log', encoding='utf-8') as file: with open('only.log', encoding='utf-8') as file:
for line in file: for i, line in enumerate(file):
line = line.split(' : ') line = line.split(' : ')
event = line[0].strip().split(' ') event = line[0].strip().split(' ')
@ -290,6 +306,4 @@ def logfile():
log_dict[i] = {'event': event_data, 'message': message_data} log_dict[i] = {'event': event_data, 'message': message_data}
i += 1 # Line number, starts at 0
return jsonify(log_dict) return jsonify(log_dict)

View file

@ -5,8 +5,8 @@ function showLogin() {
'Need an account? <span class="pop-up__link" onclick="showRegister()">Register!</span>', 'Need an account? <span class="pop-up__link" onclick="showRegister()">Register!</span>',
'<button class="btn-block primary" form="loginForm" type="submit">Login</button>', '<button class="btn-block primary" form="loginForm" type="submit">Login</button>',
'<form id="loginForm" onsubmit="return login(event)">\ '<form id="loginForm" onsubmit="return login(event)">\
<input class="pop-up__input" type="text" placeholder="Namey" id="username"/>\ <input class="input-block" type="text" placeholder="Namey" id="username"/>\
<input class="pop-up__input" type="password" placeholder="Passywassy" id="password"/>\ <input class="input-block" type="password" placeholder="Passywassy" id="password"/>\
</form>' </form>'
); );
}; };
@ -59,10 +59,10 @@ function showRegister() {
'Already have an account? <span class="pop-up__link" onclick="showLogin()">Login!</span>', 'Already have an account? <span class="pop-up__link" onclick="showLogin()">Login!</span>',
'<button class="btn-block primary" form="registerForm" type="submit">Register</button>', '<button class="btn-block primary" form="registerForm" type="submit">Register</button>',
'<form id="registerForm" onsubmit="return register(event)">\ '<form id="registerForm" onsubmit="return register(event)">\
<input class="pop-up__input" type="text" placeholder="Namey" id="username"/>\ <input class="input-block" type="text" placeholder="Namey" id="username"/>\
<input class="pop-up__input" type="text" placeholder="E mail!" id="email"/>\ <input class="input-block" type="text" placeholder="E mail!" id="email"/>\
<input class="pop-up__input" type="password" placeholder="Passywassy" id="password"/>\ <input class="input-block" type="password" placeholder="Passywassy" id="password"/>\
<input class="pop-up__input" type="password" placeholder="Passywassy again!" id="password-repeat"/>\ <input class="input-block" type="password" placeholder="Passywassy again!" id="password-repeat"/>\
</form>' </form>'
); );
}; };

View file

@ -35,7 +35,7 @@ function loadOnView() {
let image = lazyLoad[i]; let image = lazyLoad[i];
if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) { if (image.getBoundingClientRect().top < window.innerHeight && image.getBoundingClientRect().bottom > 0) {
if (!image.src) { if (!image.src) {
image.src = `/api/uploads/${image.getAttribute('data-src')}?w=400&h=400` image.src = `/api/file/${image.getAttribute('data-src')}?w=400&h=400`
} }
} }
} }

View file

@ -6,7 +6,7 @@
<div class="banner"> <div class="banner">
{% if images %} {% if images %}
<img <img
src="/api/uploads/{{ images.0.file_name }}?w=1920&h=1080" src="/api/file/{{ images.0.file_name }}?w=1920&h=1080"
onload="imgFade(this)" onload="imgFade(this)"
style="opacity:0; background-color:rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }})" style="opacity:0; background-color:rgb({{ images.0.image_colours.0.0 }}, {{ images.0.image_colours.0.1 }}, {{ images.0.image_colours.0.2 }})"
/> />

View file

@ -4,7 +4,7 @@
{% block content %} {% block content %}
<div class="background"> <div class="background">
<img src="/api/uploads/{{ image.file_name }}?w=1920&h=1080" alt="{{ image.post_alt }}" onload="imgFade(this)" style="opacity:0;"/> <img src="/api/file/{{ image.file_name }}?w=1920&h=1080" alt="{{ image.post_alt }}" onload="imgFade(this)" style="opacity:0;"/>
<span style="background-image: linear-gradient(to top, rgba({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }}, 1), transparent);"></span> <span style="background-image: linear-gradient(to top, rgba({{ image.image_colours.0.0 }}, {{ image.image_colours.0.1 }}, {{ image.image_colours.0.2 }}, 1), transparent);"></span>
</div> </div>
@ -15,7 +15,7 @@
<div class="image-grid"> <div class="image-grid">
<div class="image-container" id="image-container"> <div class="image-container" id="image-container">
<img <img
src="/api/uploads/{{ image.file_name }}?w=1920&h=1080" src="/api/file/{{ image.file_name }}?w=1920&h=1080"
alt="{{ image.post_alt }}" alt="{{ image.post_alt }}"
onload="imgFade(this)" style="opacity:0;" onload="imgFade(this)" style="opacity:0;"
onerror="this.src='/static/images/error.png'" onerror="this.src='/static/images/error.png'"
@ -55,7 +55,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>
</span> </span>
</button> </button>
<a class="pill-item" href="/api/uploads/{{ image.file_name }}" download onclick="addNotification('Download started!', 4)"> <a class="pill-item" href="/api/file/{{ image.file_name }}" download onclick="addNotification('Download started!', 4)">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M232,136v64a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V136a8,8,0,0,1,8-8H224A8,8,0,0,1,232,136Z" opacity="0.2"></path><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H72a8,8,0,0,1,0,16H32v64H224V136H184a8,8,0,0,1,0-16h40A16,16,0,0,1,240,136Zm-117.66-2.34a8,8,0,0,0,11.32,0l48-48a8,8,0,0,0-11.32-11.32L136,108.69V24a8,8,0,0,0-16,0v84.69L85.66,74.34A8,8,0,0,0,74.34,85.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M232,136v64a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V136a8,8,0,0,1,8-8H224A8,8,0,0,1,232,136Z" opacity="0.2"></path><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H72a8,8,0,0,1,0,16H32v64H224V136H184a8,8,0,0,1,0-16h40A16,16,0,0,1,240,136Zm-117.66-2.34a8,8,0,0,0,11.32,0l48-48a8,8,0,0,0-11.32-11.32L136,108.69V24a8,8,0,0,0-16,0v84.69L85.66,74.34A8,8,0,0,0,74.34,85.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
<span class="tool-tip"> <span class="tool-tip">
Download Download
@ -237,7 +237,7 @@
document.querySelector("html").style.overflow = "hidden"; document.querySelector("html").style.overflow = "hidden";
let fullscreen = document.querySelector('.image-fullscreen') let fullscreen = document.querySelector('.image-fullscreen')
fullscreen.querySelector('img').src = '/api/uploads/{{ image.file_name }}'; fullscreen.querySelector('img').src = '/api/file/{{ image.file_name }}';
fullscreen.style.display = 'flex'; fullscreen.style.display = 'flex';
setTimeout(function() { setTimeout(function() {
@ -260,7 +260,7 @@
'DESTRUCTION!!!!!!', 'DESTRUCTION!!!!!!',
'This will delete the image and all of its data!!! This action is irreversible!!!!! Are you sure you want to do this?????', 'This will delete the image and all of its data!!! This action is irreversible!!!!! Are you sure you want to do this?????',
'<button class="btn-block critical" onclick="deleteImage()">Dewww eeeet!</button>', '<button class="btn-block critical" onclick="deleteImage()">Dewww eeeet!</button>',
'<img src="/api/uploads/{{ image.file_name }}?w=1920&h=1080" />' '<img src="/api/file/{{ image.file_name }}?w=1920&h=1080" />'
); );
}); });
function deleteImage() { function deleteImage() {

View file

@ -15,7 +15,7 @@
transition: opacity 0.2s ease transition: opacity 0.2s ease
.pop-up__click-off .pop-up__click-off
width: 100vw width: 100%
height: 100vh height: 100vh
position: absolute position: absolute
@ -52,7 +52,6 @@
width: 100% width: 100%
height: auto height: auto
max-height: 50vh
display: flex display: flex
flex-direction: column flex-direction: column
@ -151,12 +150,9 @@
left: 0 left: 0
bottom: 0 bottom: 0
backdrop-filter: blur(0.5rem)
.pop-up-wrapper .pop-up-wrapper
width: calc(100vw - 1rem) width: calc(100vw - 1rem)
max-height: calc(100vh - 1rem) max-height: 99vh
max-height: calc(100dvh - 1rem)
left: 0.5rem left: 0.5rem
bottom: 0.5rem bottom: 0.5rem

View file

@ -3,6 +3,7 @@
padding: 0 0 0 3.5rem padding: 0 0 0 3.5rem
width: 100% width: 100%
height: 100%
height: 100dvh height: 100dvh
position: fixed position: fixed

View file

@ -3,6 +3,7 @@
padding: 0 padding: 0
width: 3.5rem width: 3.5rem
height: 100%
height: 100dvh height: 100dvh
display: flex display: flex

View file

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "onlylegs" name = "onlylegs"
version = "23.03.12" version = "23.03.14"
description = "Gallery built for fast and simple image management" description = "Gallery built for fast and simple image management"
authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"] authors = ["Fluffy-Bean <michal-gdula@protonmail.com>"]
license = "MIT" license = "MIT"

2
run.py
View file

@ -5,7 +5,7 @@ print("""
| |_| | | | | | |_| | |__| __/ (_| \__ \ | |_| | | | | | |_| | |__| __/ (_| \__ \
\___/|_| |_|_|\__, |_____\___|\__, |___/ \___/|_| |_|_|\__, |_____\___|\__, |___/
|___/ |___/ |___/ |___/
Created by Fluffy Bean - Version 23.03.12 Created by Fluffy Bean - Version 23.03.14
""") """)

View file

@ -46,6 +46,7 @@ class Configuration:
try: try:
os.makedirs(USER_DIR) os.makedirs(USER_DIR)
os.makedirs(os.path.join(USER_DIR, 'instance')) os.makedirs(os.path.join(USER_DIR, 'instance'))
os.makedirs(os.path.join(USER_DIR, 'uploads'))
except Exception as err: except Exception as err:
print("Error creating user directory:", err) print("Error creating user directory:", err)
sys.exit(1) sys.exit(1)