Compare commits
10 commits
a308c10767
...
296cf5550a
| Author | SHA1 | Date | |
|---|---|---|---|
| 296cf5550a | |||
| 14b2849b39 | |||
| 160ec20fab | |||
| 0a136250d9 | |||
| eb4c48cbca | |||
| d040c65af6 | |||
| 48552690dc | |||
| e92a2d9b57 | |||
| d80abcc945 | |||
| 31de5d0dfa |
|
|
@ -41,7 +41,7 @@ def decrypt_file(aes, input_handle, output_handle):
|
||||||
last_byte = chunk[-1]
|
last_byte = chunk[-1]
|
||||||
while chunk and chunk[-1] == last_byte:
|
while chunk and chunk[-1] == last_byte:
|
||||||
chunk = chunk[:-1]
|
chunk = chunk[:-1]
|
||||||
if bytes_read % bytestring.MIBIBYTE == 0:
|
if bytes_read % bytestring.MEBIBYTE == 0:
|
||||||
print(bytestring.bytestring(bytes_read))
|
print(bytestring.bytestring(bytes_read))
|
||||||
output_handle.write(chunk)
|
output_handle.write(chunk)
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ def encrypt_file(aes, input_handle, output_handle):
|
||||||
chunk += pad_byte * (BLOCK_SIZE - len(chunk))
|
chunk += pad_byte * (BLOCK_SIZE - len(chunk))
|
||||||
done = True
|
done = True
|
||||||
bytes_read += len(chunk)
|
bytes_read += len(chunk)
|
||||||
if bytes_read % bytestring.MIBIBYTE == 0:
|
if bytes_read % bytestring.MEBIBYTE == 0:
|
||||||
print(bytestring.bytestring(bytes_read))
|
print(bytestring.bytestring(bytes_read))
|
||||||
chunk = aes.encrypt(chunk)
|
chunk = aes.encrypt(chunk)
|
||||||
output_handle.write(chunk)
|
output_handle.write(chunk)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ var image_height = 200;
|
||||||
var video_height = 300;
|
var video_height = 300;
|
||||||
var audio_width = 1000;
|
var audio_width = 1000;
|
||||||
var IMAGE_TYPES = ["\\.jpg", "\\.jpeg", "\\.jpg", "\\.bmp", "\\.tiff", "\\.tif", "\\.bmp", "\\.gif", "\\.png", "reddituploads\.com", "\\.webp", "drscdn\\.500px\\.org\\/photo"].join("|");
|
var IMAGE_TYPES = ["\\.jpg", "\\.jpeg", "\\.jpg", "\\.bmp", "\\.tiff", "\\.tif", "\\.bmp", "\\.gif", "\\.png", "reddituploads\.com", "\\.webp", "drscdn\\.500px\\.org\\/photo"].join("|");
|
||||||
var AUDIO_TYPES = ["\\.aac", "\\.flac", "\\.mp3", "\\.m4a", "\\.ogg", "\\.opus", "\\.wav"].join("|");
|
var AUDIO_TYPES = ["\\.aac", "\\.flac", "\\.mp3", "\\.m4a", "\\.mka", "\\.ogg", "\\.opus", "\\.wav"].join("|");
|
||||||
var VIDEO_TYPES = ["\\.mp4", "\\.m4v", "\\.mkv", "\\.webm", "\\.ogv"].join("|");
|
var VIDEO_TYPES = ["\\.mp4", "\\.m4v", "\\.mkv", "\\.webm", "\\.ogv"].join("|");
|
||||||
IMAGE_TYPES = new RegExp(IMAGE_TYPES, "i");
|
IMAGE_TYPES = new RegExp(IMAGE_TYPES, "i");
|
||||||
AUDIO_TYPES = new RegExp(AUDIO_TYPES, "i");
|
AUDIO_TYPES = new RegExp(AUDIO_TYPES, "i");
|
||||||
|
|
@ -18,6 +18,8 @@ VIDEO_TYPES = new RegExp(VIDEO_TYPES, "i");
|
||||||
var has_started = false;
|
var has_started = false;
|
||||||
|
|
||||||
var CSS = `
|
var CSS = `
|
||||||
|
* { box-sizing: inherit; }
|
||||||
|
html { box-sizing: border-box; }
|
||||||
body { background-color: #fff; }
|
body { background-color: #fff; }
|
||||||
audio, video { display: block; }
|
audio, video { display: block; }
|
||||||
audio { width: ${audio_width}px; max-width: 100% }
|
audio { width: ${audio_width}px; max-width: 100% }
|
||||||
|
|
@ -30,7 +32,7 @@ a { color: #000 !important; }
|
||||||
.delete_button { color: #d00; font-family: Arial; font-size: 11px; left: 0; position: absolute; top: 0; width: 25px; }
|
.delete_button { color: #d00; font-family: Arial; font-size: 11px; left: 0; position: absolute; top: 0; width: 25px; }
|
||||||
.ingest { position:absolute; right: 5px; top: 5px; height: 100%; width: 30% }
|
.ingest { position:absolute; right: 5px; top: 5px; height: 100%; width: 30% }
|
||||||
.ingestbox { position:relative; height: 75%; width:100%; box-sizing: border-box; }
|
.ingestbox { position:relative; height: 75%; width:100%; box-sizing: border-box; }
|
||||||
.urldumpbox { overflow-y: scroll; height: 300px; width: 90% }
|
.urldumpbox { overflow-y: scroll; height: 300px; width: 100% }
|
||||||
.load_button { position: absolute; top: 10%; width: 100%; height: 80%; word-wrap: break-word; }
|
.load_button { position: absolute; top: 10%; width: 100%; height: 80%; word-wrap: break-word; }
|
||||||
.odi_anchor { display: block; }
|
.odi_anchor { display: block; }
|
||||||
.odi_image_div, .odi_media_div { display: inline-block; margin: 5px; float: left; position: relative; background-color: #aaa; }
|
.odi_image_div, .odi_media_div { display: inline-block; margin: 5px; float: left; position: relative; background-color: #aaa; }
|
||||||
|
|
@ -281,6 +283,7 @@ function create_odi_div(url)
|
||||||
button.onclick = function()
|
button.onclick = function()
|
||||||
{
|
{
|
||||||
delete_odi_div(this);
|
delete_odi_div(this);
|
||||||
|
dump_urls();
|
||||||
};
|
};
|
||||||
div.appendChild(button);
|
div.appendChild(button);
|
||||||
return div;
|
return div;
|
||||||
|
|
@ -332,7 +335,7 @@ function create_workspace()
|
||||||
var heightfilter = create_command_box_button("heightfilter", "min height", filter_height);
|
var heightfilter = create_command_box_button("heightfilter", "min height", filter_height);
|
||||||
var widthfilter = create_command_box_button("widthfilter", "min width", filter_width);
|
var widthfilter = create_command_box_button("widthfilter", "min width", filter_width);
|
||||||
var sorter = create_command_button("sort size", sort_size);
|
var sorter = create_command_button("sort size", sort_size);
|
||||||
var dumper = create_command_button("dump urls", dump_urls);
|
/*var dumper = create_command_button("dump urls", dump_urls);*/
|
||||||
var ingest_box = document.createElement("textarea");
|
var ingest_box = document.createElement("textarea");
|
||||||
var ingest_button = create_command_button("ingest", ingest);
|
var ingest_button = create_command_button("ingest", ingest);
|
||||||
var start_button = create_command_button("load all", function(){start();});
|
var start_button = create_command_button("load all", function(){start();});
|
||||||
|
|
@ -362,7 +365,7 @@ function create_workspace()
|
||||||
control_panel.appendChild(heightfilter);
|
control_panel.appendChild(heightfilter);
|
||||||
control_panel.appendChild(widthfilter);
|
control_panel.appendChild(widthfilter);
|
||||||
control_panel.appendChild(sorter);
|
control_panel.appendChild(sorter);
|
||||||
control_panel.appendChild(dumper);
|
/*control_panel.appendChild(dumper);*/
|
||||||
control_panel.appendChild(ingest_div);
|
control_panel.appendChild(ingest_div);
|
||||||
control_panel.appendChild(start_button);
|
control_panel.appendChild(start_button);
|
||||||
document.body.appendChild(workspace);
|
document.body.appendChild(workspace);
|
||||||
|
|
@ -431,6 +434,7 @@ function filter_dimension(dimension, minimum)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dump_urls();
|
||||||
}
|
}
|
||||||
|
|
||||||
function filter_height(minimum)
|
function filter_height(minimum)
|
||||||
|
|
@ -462,6 +466,32 @@ function filter_re(pattern, do_delete)
|
||||||
delete_odi_div(div);
|
delete_odi_div(div);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dump_urls();
|
||||||
|
}
|
||||||
|
|
||||||
|
const REJECT_PATTERNS = [
|
||||||
|
"fuskator.com/images/",
|
||||||
|
"fuskator.com/small/",
|
||||||
|
"thumbs.redditmedia",
|
||||||
|
"redditstatic.com/mailgray.png",
|
||||||
|
"redditstatic.com/start_chat.png",
|
||||||
|
"preview.redd.it/award_images",
|
||||||
|
"redditstatic.com/gold/awards",
|
||||||
|
"pixel.reddit",
|
||||||
|
"/thumb/",
|
||||||
|
"/loaders/",
|
||||||
|
"memegen",
|
||||||
|
];
|
||||||
|
|
||||||
|
function match_any_reject(url)
|
||||||
|
{
|
||||||
|
for (const pattern of REJECT_PATTERNS)
|
||||||
|
{
|
||||||
|
if (url.match(pattern))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_all_urls()
|
function get_all_urls()
|
||||||
|
|
@ -479,27 +509,11 @@ function get_all_urls()
|
||||||
if (seen_urls.has(url))
|
if (seen_urls.has(url))
|
||||||
{continue;}
|
{continue;}
|
||||||
console.log(url);
|
console.log(url);
|
||||||
if (url.indexOf("thumbs.redditmedia") != -1)
|
if (match_any_reject(url))
|
||||||
{console.log("Rejecting reddit thumb"); continue;}
|
{
|
||||||
if (url.indexOf("redditstatic.com/mailgray.png") != -1)
|
console.log("Rejecting.");
|
||||||
{console.log("Rejecting reddit icons"); continue;}
|
continue;
|
||||||
if (url.indexOf("redditstatic.com/start_chat.png") != -1)
|
}
|
||||||
{console.log("Rejecting reddit icons"); continue;}
|
|
||||||
if (url.indexOf("preview.redd.it/award_images") != -1)
|
|
||||||
{console.log("Rejecting reddit awards"); continue;}
|
|
||||||
if (url.indexOf("redditstatic.com/gold/awards") != -1)
|
|
||||||
{console.log("Rejecting reddit awards"); continue;}
|
|
||||||
if (url.indexOf("pixel.reddit") != -1 || url.indexOf("reddit.com/static/pixel") != -1)
|
|
||||||
{console.log("Rejecting reddit pixel"); continue}
|
|
||||||
if (url.indexOf("/thumb/") != -1)
|
|
||||||
{console.log("Rejecting /thumb/"); continue;}
|
|
||||||
if (url.indexOf("/loaders/") != -1)
|
|
||||||
{console.log("Rejecting loader"); continue;}
|
|
||||||
if (url.indexOf("memegen") != -1)
|
|
||||||
{console.log("Rejecting retardation"); continue;}
|
|
||||||
if (url.indexOf("4cdn") != -1 && url.indexOf("s.jpg") != -1)
|
|
||||||
{console.log("Rejecting 4chan thumb"); continue;}
|
|
||||||
|
|
||||||
sub_urls = normalize_url(url);
|
sub_urls = normalize_url(url);
|
||||||
if (sub_urls == null)
|
if (sub_urls == null)
|
||||||
{continue;}
|
{continue;}
|
||||||
|
|
@ -688,15 +702,19 @@ function lazy_load_one(element, comeback)
|
||||||
width = this.naturalWidth;
|
width = this.naturalWidth;
|
||||||
height = this.naturalHeight;
|
height = this.naturalHeight;
|
||||||
if (width == 161 && height == 81)
|
if (width == 161 && height == 81)
|
||||||
{delete_odi_div(this);}
|
{
|
||||||
|
delete_odi_div(this);
|
||||||
|
}
|
||||||
this.arealabel.innerHTML = width + " x " + height;
|
this.arealabel.innerHTML = width + " x " + height;
|
||||||
this.odi_div.style.minWidth = "0px";
|
this.odi_div.style.minWidth = "0px";
|
||||||
if (comeback){lazy_load_all()};
|
if (comeback){lazy_load_all()};
|
||||||
|
dump_urls();
|
||||||
};
|
};
|
||||||
image.onerror = function()
|
image.onerror = function()
|
||||||
{
|
{
|
||||||
delete_odi_div(this);
|
delete_odi_div(this);
|
||||||
if (comeback){lazy_load_all()};
|
if (comeback){lazy_load_all()};
|
||||||
|
dump_urls();
|
||||||
};
|
};
|
||||||
/*console.log("Lazy loading " + element.lazy_src)*/
|
/*console.log("Lazy loading " + element.lazy_src)*/
|
||||||
image.src = image.lazy_src;
|
image.src = image.lazy_src;
|
||||||
|
|
@ -869,6 +887,7 @@ function main()
|
||||||
var divs = create_odi_divs(all_urls);
|
var divs = create_odi_divs(all_urls);
|
||||||
create_workspace();
|
create_workspace();
|
||||||
fill_workspace(divs);
|
fill_workspace(divs);
|
||||||
|
dump_urls();
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
BSD 3-Clause License
|
BSD 3-Clause License
|
||||||
|
|
||||||
Copyright (c) 2021, Ethan Dalool aka voussoir
|
Copyright (c) 2022, Ethan Dalool aka voussoir
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from voussoirkit import bytestring
|
||||||
from voussoirkit import downloady
|
from voussoirkit import downloady
|
||||||
from voussoirkit import pipeable
|
from voussoirkit import pipeable
|
||||||
|
|
||||||
DEFAULT_PIECE_SIZE = bytestring.MIBIBYTE
|
DEFAULT_PIECE_SIZE = bytestring.MEBIBYTE
|
||||||
DEFAULT_THREAD_COUNT = 10
|
DEFAULT_THREAD_COUNT = 10
|
||||||
|
|
||||||
def init(url, localname=None, piece_size=DEFAULT_PIECE_SIZE):
|
def init(url, localname=None, piece_size=DEFAULT_PIECE_SIZE):
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from voussoirkit import passwordy
|
||||||
from voussoirkit import pathclass
|
from voussoirkit import pathclass
|
||||||
from voussoirkit import ratelimiter
|
from voussoirkit import ratelimiter
|
||||||
|
|
||||||
CHUNK_SIZE = bytestring.MIBIBYTE
|
CHUNK_SIZE = bytestring.MEBIBYTE
|
||||||
|
|
||||||
OPENDIR_TEMPLATE = '''
|
OPENDIR_TEMPLATE = '''
|
||||||
<html>
|
<html>
|
||||||
|
|
@ -203,9 +203,14 @@ class RequestHandler(http.server.BaseHTTPRequestHandler):
|
||||||
response = read_filebytes(path, range_min=range_min, range_max=range_max)
|
response = read_filebytes(path, range_min=range_min, range_max=range_max)
|
||||||
|
|
||||||
elif path.is_dir:
|
elif path.is_dir:
|
||||||
headers['Content-type'] = 'text/html'
|
if self.server.enable_directory_listings:
|
||||||
response = generate_opendir(path, enable_zip=self.server.enable_zip)
|
headers['Content-type'] = 'text/html'
|
||||||
response = response.encode('utf-8')
|
response = generate_opendir(path, enable_zip=self.server.enable_zip)
|
||||||
|
response = response.encode('utf-8')
|
||||||
|
else:
|
||||||
|
status_code = 404
|
||||||
|
self.send_error(status_code)
|
||||||
|
response = bytes()
|
||||||
|
|
||||||
elif self.path.endswith('.zip') and self.server.enable_zip:
|
elif self.path.endswith('.zip') and self.server.enable_zip:
|
||||||
path = url_to_path(self.path.rsplit('.zip', 1)[0])
|
path = url_to_path(self.path.rsplit('.zip', 1)[0])
|
||||||
|
|
@ -303,12 +308,14 @@ class SimpleServer:
|
||||||
password,
|
password,
|
||||||
authorize_by_ip,
|
authorize_by_ip,
|
||||||
enable_zip,
|
enable_zip,
|
||||||
|
enable_directory_listings,
|
||||||
overall_ratelimit,
|
overall_ratelimit,
|
||||||
individual_ratelimit,
|
individual_ratelimit,
|
||||||
):
|
):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.password = password
|
self.password = password
|
||||||
self.authorize_by_ip = authorize_by_ip
|
self.authorize_by_ip = authorize_by_ip
|
||||||
|
self.enable_directory_listings = enable_directory_listings
|
||||||
self.enable_zip = enable_zip
|
self.enable_zip = enable_zip
|
||||||
self.overall_ratelimit = ratelimiter.Ratelimiter(overall_ratelimit)
|
self.overall_ratelimit = ratelimiter.Ratelimiter(overall_ratelimit)
|
||||||
self.individual_ratelimit = individual_ratelimit
|
self.individual_ratelimit = individual_ratelimit
|
||||||
|
|
@ -483,62 +490,97 @@ def zip_directory(path):
|
||||||
|
|
||||||
# COMMAND LINE #####################################################################################
|
# COMMAND LINE #####################################################################################
|
||||||
|
|
||||||
DOCSTRING = '''
|
|
||||||
simpleserver
|
|
||||||
============
|
|
||||||
|
|
||||||
Run a simple file server from your computer.
|
|
||||||
|
|
||||||
> simpleserver port <flags>
|
|
||||||
|
|
||||||
port:
|
|
||||||
Port number, an integer.
|
|
||||||
|
|
||||||
flags:
|
|
||||||
--password X:
|
|
||||||
A password string. The user will be prompted to enter it before proceeding
|
|
||||||
to any URL. A token is stored in a cookie unless authorize_by_ip is used.
|
|
||||||
|
|
||||||
--authorize_by_ip:
|
|
||||||
After the user enters the password, their entire IP becomes authorized for
|
|
||||||
all future requests. This reduces security, because a single IP can be home
|
|
||||||
to many different people, but increases convenience, because the user can
|
|
||||||
use download managers / scripts where adding auth is not convenient.
|
|
||||||
|
|
||||||
--enable_zip:
|
|
||||||
Add a 'zip' link to every directory and allow the user to download the
|
|
||||||
entire directory as a zip file.
|
|
||||||
|
|
||||||
--overall_ratelimit X:
|
|
||||||
An integer number of bytes, the maximum bytes/sec of the server overall.
|
|
||||||
|
|
||||||
--individual_ratelimit X:
|
|
||||||
An integer number of bytes, the maximum bytes/sec for any single request.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def simpleserver_argparse(args):
|
def simpleserver_argparse(args):
|
||||||
server = SimpleServer(
|
server = SimpleServer(
|
||||||
port=args.port,
|
port=args.port,
|
||||||
password=args.password,
|
password=args.password,
|
||||||
authorize_by_ip=args.authorize_by_ip,
|
authorize_by_ip=args.authorize_by_ip,
|
||||||
enable_zip=args.enable_zip,
|
enable_zip=args.enable_zip,
|
||||||
|
enable_directory_listings=args.enable_directory_listings,
|
||||||
overall_ratelimit=args.overall_ratelimit,
|
overall_ratelimit=args.overall_ratelimit,
|
||||||
individual_ratelimit=args.individual_ratelimit,
|
individual_ratelimit=args.individual_ratelimit,
|
||||||
)
|
)
|
||||||
server.start()
|
server.start()
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
parser = argparse.ArgumentParser(description=DOCSTRING)
|
parser = argparse.ArgumentParser(
|
||||||
|
description=
|
||||||
parser.add_argument('port', nargs='?', type=int, default=40000)
|
'''
|
||||||
parser.add_argument('--password', dest='password', default=None)
|
Run a simple http file server from your computer.
|
||||||
parser.add_argument('--authorize_by_ip', '--authorize-by-ip', dest='authorize_by_ip', action='store_true')
|
''',
|
||||||
parser.add_argument('--enable_zip', '--enable-zip', dest='enable_zip', action='store_true')
|
)
|
||||||
parser.add_argument('--overall_ratelimit', '--overall-ratelimit', type=bytestring.parsebytes, default=20*bytestring.MIBIBYTE)
|
parser.examples = [
|
||||||
parser.add_argument('--individual_ratelimit', '--individual-ratelimit', type=bytestring.parsebytes, default=10*bytestring.MIBIBYTE)
|
'4242 --password letmeinplease --authorize-by-ip --enable-zip',
|
||||||
|
'--individual-ratelimit 2m --overall-ratelimit 10m',
|
||||||
|
]
|
||||||
|
parser.add_argument(
|
||||||
|
'port',
|
||||||
|
nargs='?',
|
||||||
|
type=int,
|
||||||
|
default=40000,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--password',
|
||||||
|
dest='password',
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help='''
|
||||||
|
A password string. The user will be prompted to enter it before proceeding
|
||||||
|
to any URL. A token is stored in a cookie unless authorize_by_ip is used.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--authorize_by_ip',
|
||||||
|
'--authorize-by-ip',
|
||||||
|
action='store_true',
|
||||||
|
help='''
|
||||||
|
After the user enters the password, their entire IP becomes authorized for
|
||||||
|
all future requests. This reduces security, because a single IP can be home
|
||||||
|
to many different people, but increases convenience, because the user can
|
||||||
|
use download managers / scripts where adding auth is not convenient.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--enable_zip',
|
||||||
|
'--enable-zip',
|
||||||
|
action='store_true',
|
||||||
|
help='''
|
||||||
|
Add a 'zip' link to every directory and allow the user to download the
|
||||||
|
entire directory as a zip file.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--overall_ratelimit',
|
||||||
|
'--overall-ratelimit',
|
||||||
|
type=bytestring.parsebytes,
|
||||||
|
default=200*bytestring.MEBIBYTE,
|
||||||
|
help='''
|
||||||
|
The maximum bytes/sec of the server overall.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--no_directories',
|
||||||
|
'--no-directories',
|
||||||
|
dest='enable_directory_listings',
|
||||||
|
action='store_false',
|
||||||
|
default=True,
|
||||||
|
help='''
|
||||||
|
Disable the generation of directory listing pages. They will return 404
|
||||||
|
instead. The visitor must have the exact link to a file.
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--individual_ratelimit',
|
||||||
|
'--individual-ratelimit',
|
||||||
|
type=bytestring.parsebytes,
|
||||||
|
default=100*bytestring.MEBIBYTE,
|
||||||
|
help='''
|
||||||
|
The maximum bytes/sec for any single request.
|
||||||
|
''',
|
||||||
|
)
|
||||||
parser.set_defaults(func=simpleserver_argparse)
|
parser.set_defaults(func=simpleserver_argparse)
|
||||||
|
|
||||||
return betterhelp.single_main(argv, parser, DOCSTRING)
|
return betterhelp.go(parser, argv)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
raise SystemExit(main(sys.argv[1:]))
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 132 B |
|
|
@ -1,51 +0,0 @@
|
||||||
import flask
|
|
||||||
from flask import request
|
|
||||||
import functools
|
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
def _generate_session_token():
|
|
||||||
token = str(uuid.uuid4())
|
|
||||||
#print('MAKE SESSION', token)
|
|
||||||
return token
|
|
||||||
|
|
||||||
def give_session_token(function):
|
|
||||||
@functools.wraps(function)
|
|
||||||
def wrapped(*args, **kwargs):
|
|
||||||
# Inject new token so the function doesn't know the difference
|
|
||||||
token = request.cookies.get('flasksite_session', None)
|
|
||||||
if not token:
|
|
||||||
token = _generate_session_token()
|
|
||||||
request.cookies = dict(request.cookies)
|
|
||||||
request.cookies['flasksite_session'] = token
|
|
||||||
|
|
||||||
ret = function(*args, **kwargs)
|
|
||||||
|
|
||||||
# Send the token back to the client
|
|
||||||
if not isinstance(ret, flask.Response):
|
|
||||||
ret = flask.Response(ret)
|
|
||||||
ret.set_cookie('flasksite_session', value=token, max_age=60)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
def not_implemented(function):
|
|
||||||
'''
|
|
||||||
Decorator to remember what needs doing.
|
|
||||||
'''
|
|
||||||
warnings.warn('%s is not implemented' % function.__name__)
|
|
||||||
return function
|
|
||||||
|
|
||||||
def time_me(function):
|
|
||||||
'''
|
|
||||||
After the function is run, print the elapsed time.
|
|
||||||
'''
|
|
||||||
@functools.wraps(function)
|
|
||||||
def timed_function(*args, **kwargs):
|
|
||||||
start = time.time()
|
|
||||||
result = function(*args, **kwargs)
|
|
||||||
end = time.time()
|
|
||||||
print('%s: %0.8f' % (function.__name__, end-start))
|
|
||||||
return result
|
|
||||||
return timed_function
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import flask
|
|
||||||
from flask import request
|
|
||||||
import json
|
|
||||||
import mimetypes
|
|
||||||
import os
|
|
||||||
|
|
||||||
import decorators
|
|
||||||
|
|
||||||
|
|
||||||
site = flask.Flask(__name__)
|
|
||||||
site.config.update(
|
|
||||||
SEND_FILE_MAX_AGE_DEFAULT=180,
|
|
||||||
TEMPLATES_AUTO_RELOAD=True,
|
|
||||||
)
|
|
||||||
site.jinja_env.add_extension('jinja2.ext.do')
|
|
||||||
site.debug = True
|
|
||||||
|
|
||||||
|
|
||||||
####################################################################################################
|
|
||||||
####################################################################################################
|
|
||||||
####################################################################################################
|
|
||||||
####################################################################################################
|
|
||||||
|
|
||||||
def make_json_response(j, *args, **kwargs):
|
|
||||||
dumped = json.dumps(j)
|
|
||||||
response = flask.Response(dumped, *args, **kwargs)
|
|
||||||
response.headers['Content-Type'] = 'application/json;charset=utf-8'
|
|
||||||
return response
|
|
||||||
|
|
||||||
def send_file(filepath):
|
|
||||||
'''
|
|
||||||
Range-enabled file sending.
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
file_size = os.path.getsize(filepath)
|
|
||||||
except FileNotFoundError:
|
|
||||||
flask.abort(404)
|
|
||||||
|
|
||||||
outgoing_headers = {}
|
|
||||||
mimetype = mimetypes.guess_type(filepath)[0]
|
|
||||||
if mimetype is not None:
|
|
||||||
if 'text/' in mimetype:
|
|
||||||
mimetype += '; charset=utf-8'
|
|
||||||
outgoing_headers['Content-Type'] = mimetype
|
|
||||||
|
|
||||||
if 'range' in request.headers:
|
|
||||||
desired_range = request.headers['range'].lower()
|
|
||||||
desired_range = desired_range.split('bytes=')[-1]
|
|
||||||
|
|
||||||
int_helper = lambda x: int(x) if x.isdigit() else None
|
|
||||||
if '-' in desired_range:
|
|
||||||
(desired_min, desired_max) = desired_range.split('-')
|
|
||||||
range_min = int_helper(desired_min)
|
|
||||||
range_max = int_helper(desired_max)
|
|
||||||
else:
|
|
||||||
range_min = int_helper(desired_range)
|
|
||||||
|
|
||||||
if range_min is None:
|
|
||||||
range_min = 0
|
|
||||||
if range_max is None:
|
|
||||||
range_max = file_size
|
|
||||||
|
|
||||||
# because ranges are 0-indexed
|
|
||||||
range_max = min(range_max, file_size - 1)
|
|
||||||
range_min = max(range_min, 0)
|
|
||||||
|
|
||||||
range_header = 'bytes {min}-{max}/{outof}'.format(
|
|
||||||
min=range_min,
|
|
||||||
max=range_max,
|
|
||||||
outof=file_size,
|
|
||||||
)
|
|
||||||
outgoing_headers['Content-Range'] = range_header
|
|
||||||
status = 206
|
|
||||||
else:
|
|
||||||
range_max = file_size - 1
|
|
||||||
range_min = 0
|
|
||||||
status = 200
|
|
||||||
|
|
||||||
outgoing_headers['Accept-Ranges'] = 'bytes'
|
|
||||||
outgoing_headers['Content-Length'] = (range_max - range_min) + 1
|
|
||||||
|
|
||||||
if request.method == 'HEAD':
|
|
||||||
outgoing_data = bytes()
|
|
||||||
else:
|
|
||||||
outgoing_data = helpers.read_filebytes(filepath, range_min=range_min, range_max=range_max)
|
|
||||||
|
|
||||||
response = flask.Response(
|
|
||||||
outgoing_data,
|
|
||||||
status=status,
|
|
||||||
headers=outgoing_headers,
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
####################################################################################################
|
|
||||||
####################################################################################################
|
|
||||||
####################################################################################################
|
|
||||||
####################################################################################################
|
|
||||||
|
|
||||||
@site.route('/')
|
|
||||||
@decorators.give_session_token
|
|
||||||
def root():
|
|
||||||
return flask.render_template('root.html')
|
|
||||||
|
|
||||||
@site.route('/favicon.ico')
|
|
||||||
@site.route('/favicon.png')
|
|
||||||
def favicon():
|
|
||||||
filename = os.path.join('static', 'favicon.png')
|
|
||||||
return flask.send_file(filename)
|
|
||||||
|
|
||||||
@site.route('/static/<filename>')
|
|
||||||
def get_static(filename):
|
|
||||||
filename = filename.replace('\\', os.sep)
|
|
||||||
filename = filename.replace('/', os.sep)
|
|
||||||
filename = os.path.join('static', filename)
|
|
||||||
return flask.send_file(filename)
|
|
||||||
|
|
||||||
@site.route("/float/<float:value>")
|
|
||||||
def float_type(value):
|
|
||||||
print(value + 1)
|
|
||||||
return "correct"
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
#site.run(threaded=True)
|
|
||||||
pass
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import gevent.monkey
|
|
||||||
gevent.monkey.patch_all()
|
|
||||||
|
|
||||||
import flasksite
|
|
||||||
import gevent.pywsgi
|
|
||||||
import gevent.wsgi
|
|
||||||
import sys
|
|
||||||
|
|
||||||
if len(sys.argv) == 2:
|
|
||||||
port = int(sys.argv[1])
|
|
||||||
else:
|
|
||||||
port = 5000
|
|
||||||
|
|
||||||
if port == 443:
|
|
||||||
http = gevent.pywsgi.WSGIServer(
|
|
||||||
listener=('0.0.0.0', port),
|
|
||||||
application=flasksite.site,
|
|
||||||
keyfile='https\\flasksite.key',
|
|
||||||
certfile='https\\flasksite.crt',
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
http = gevent.pywsgi.WSGIServer(
|
|
||||||
listener=('0.0.0.0', port),
|
|
||||||
application=flasksite.site,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
print('Starting server on port %d' % port)
|
|
||||||
http.serve_forever()
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
import math
|
|
||||||
|
|
||||||
FILE_READ_CHUNK = 512 * 1024
|
|
||||||
|
|
||||||
def chunk_sequence(sequence, chunk_length, allow_incomplete=True):
|
|
||||||
'''
|
|
||||||
Given a sequence, divide it into sequences of length `chunk_length`.
|
|
||||||
|
|
||||||
allow_incomplete:
|
|
||||||
If True, allow the final chunk to be shorter if the
|
|
||||||
given sequence is not an exact multiple of `chunk_length`.
|
|
||||||
If False, the incomplete chunk will be discarded.
|
|
||||||
'''
|
|
||||||
(complete, leftover) = divmod(len(sequence), chunk_length)
|
|
||||||
if not allow_incomplete:
|
|
||||||
leftover = 0
|
|
||||||
|
|
||||||
chunk_count = complete + min(leftover, 1)
|
|
||||||
|
|
||||||
chunks = []
|
|
||||||
for x in range(chunk_count):
|
|
||||||
left = chunk_length * x
|
|
||||||
right = left + chunk_length
|
|
||||||
chunks.append(sequence[left:right])
|
|
||||||
|
|
||||||
return chunks
|
|
||||||
|
|
||||||
def comma_split(s):
|
|
||||||
'''
|
|
||||||
Split the string apart by commas, discarding all extra whitespace and
|
|
||||||
blank phrases.
|
|
||||||
'''
|
|
||||||
if s is None:
|
|
||||||
return s
|
|
||||||
s = s.replace(' ', ',')
|
|
||||||
s = [x.strip() for x in s.split(',')]
|
|
||||||
s = [x for x in s if x]
|
|
||||||
return s
|
|
||||||
|
|
||||||
def edit_params(original, modifications):
|
|
||||||
'''
|
|
||||||
Given a dictionary representing URL parameters,
|
|
||||||
apply the modifications and return a URL parameter string.
|
|
||||||
|
|
||||||
{'a':1, 'b':2}, {'b':3} => ?a=1&b=3
|
|
||||||
'''
|
|
||||||
new_params = original.copy()
|
|
||||||
new_params.update(modifications)
|
|
||||||
if not new_params:
|
|
||||||
return ''
|
|
||||||
new_params = ['%s=%s' % (k, v) for (k, v) in new_params.items() if v]
|
|
||||||
new_params = '&'.join(new_params)
|
|
||||||
if new_params:
|
|
||||||
new_params = '?' + new_params
|
|
||||||
return new_params
|
|
||||||
|
|
||||||
def fit_into_bounds(image_width, image_height, frame_width, frame_height):
|
|
||||||
'''
|
|
||||||
Given the w+h of the image and the w+h of the frame,
|
|
||||||
return new w+h that fits the image into the frame
|
|
||||||
while maintaining the aspect ratio.
|
|
||||||
'''
|
|
||||||
ratio = min(frame_width/image_width, frame_height/image_height)
|
|
||||||
|
|
||||||
new_width = int(image_width * ratio)
|
|
||||||
new_height = int(image_height * ratio)
|
|
||||||
|
|
||||||
return (new_width, new_height)
|
|
||||||
|
|
||||||
def hms_to_seconds(hms):
|
|
||||||
'''
|
|
||||||
Convert hh:mm:ss string to an integer seconds.
|
|
||||||
'''
|
|
||||||
hms = hms.split(':')
|
|
||||||
seconds = 0
|
|
||||||
if len(hms) == 3:
|
|
||||||
seconds += int(hms[0])*3600
|
|
||||||
hms.pop(0)
|
|
||||||
if len(hms) == 2:
|
|
||||||
seconds += int(hms[0])*60
|
|
||||||
hms.pop(0)
|
|
||||||
if len(hms) == 1:
|
|
||||||
seconds += int(hms[0])
|
|
||||||
return seconds
|
|
||||||
|
|
||||||
def is_xor(*args):
|
|
||||||
'''
|
|
||||||
Return True if and only if one arg is truthy.
|
|
||||||
'''
|
|
||||||
return [bool(a) for a in args].count(True) == 1
|
|
||||||
|
|
||||||
def read_filebytes(filepath, range_min, range_max):
|
|
||||||
'''
|
|
||||||
Yield chunks of bytes from the file between the endpoints.
|
|
||||||
'''
|
|
||||||
range_span = range_max - range_min
|
|
||||||
|
|
||||||
#print('read span', range_min, range_max, range_span)
|
|
||||||
f = open(filepath, 'rb')
|
|
||||||
f.seek(range_min)
|
|
||||||
sent_amount = 0
|
|
||||||
with f:
|
|
||||||
while sent_amount < range_span:
|
|
||||||
#print(sent_amount)
|
|
||||||
chunk = f.read(FILE_READ_CHUNK)
|
|
||||||
if len(chunk) == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
yield chunk
|
|
||||||
sent_amount += len(chunk)
|
|
||||||
|
|
||||||
def seconds_to_hms(seconds):
|
|
||||||
'''
|
|
||||||
Convert integer number of seconds to an hh:mm:ss string.
|
|
||||||
Only the necessary fields are used.
|
|
||||||
'''
|
|
||||||
seconds = math.ceil(seconds)
|
|
||||||
(minutes, seconds) = divmod(seconds, 60)
|
|
||||||
(hours, minutes) = divmod(minutes, 60)
|
|
||||||
parts = []
|
|
||||||
if hours: parts.append(hours)
|
|
||||||
if hours or minutes: parts.append(minutes)
|
|
||||||
parts.append(seconds)
|
|
||||||
hms = ':'.join('%02d' % part for part in parts)
|
|
||||||
return hms
|
|
||||||
|
|
||||||
def truthystring(s):
|
|
||||||
if isinstance(s, (bool, int)) or s is None:
|
|
||||||
return s
|
|
||||||
s = s.lower()
|
|
||||||
if s in {'1', 'true', 't', 'yes', 'y', 'on'}:
|
|
||||||
return True
|
|
||||||
if s in {'null', 'none'}:
|
|
||||||
return None
|
|
||||||
return False
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
flask
|
|
||||||
gevent
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
body
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color:#fff;
|
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
#header
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-content: center;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.header_element
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 1;
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.header_element:hover
|
|
||||||
{
|
|
||||||
background-color: #f00;
|
|
||||||
}
|
|
||||||
#content_body
|
|
||||||
{
|
|
||||||
flex: 0 0 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
function post_example(key, value)
|
|
||||||
{
|
|
||||||
var url = "/postexample";
|
|
||||||
data = new FormData();
|
|
||||||
data.append(key, value);
|
|
||||||
return post(url, data, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function post(url, data, callback)
|
|
||||||
{
|
|
||||||
var request = new XMLHttpRequest();
|
|
||||||
request.answer = null;
|
|
||||||
request.onreadystatechange = function()
|
|
||||||
{
|
|
||||||
if (request.readyState == 4)
|
|
||||||
{
|
|
||||||
var text = request.responseText;
|
|
||||||
if (callback != null)
|
|
||||||
{
|
|
||||||
console.log(text);
|
|
||||||
callback(JSON.parse(text));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var asynchronous = true;
|
|
||||||
request.open("POST", url, asynchronous);
|
|
||||||
request.send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bind_box_to_button(box, button)
|
|
||||||
{
|
|
||||||
box.onkeydown=function()
|
|
||||||
{
|
|
||||||
if (event.keyCode == 13)
|
|
||||||
{
|
|
||||||
button.click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function entry_with_history_hook(box, button)
|
|
||||||
{
|
|
||||||
//console.log(event.keyCode);
|
|
||||||
if (box.entry_history === undefined)
|
|
||||||
{box.entry_history = [];}
|
|
||||||
if (box.entry_history_pos === undefined)
|
|
||||||
{box.entry_history_pos = -1;}
|
|
||||||
if (event.keyCode == 13)
|
|
||||||
{
|
|
||||||
/* Enter */
|
|
||||||
box.entry_history.push(box.value);
|
|
||||||
button.click();
|
|
||||||
box.value = "";
|
|
||||||
}
|
|
||||||
else if (event.keyCode == 38)
|
|
||||||
{
|
|
||||||
|
|
||||||
/* Up arrow */
|
|
||||||
if (box.entry_history.length == 0)
|
|
||||||
{return}
|
|
||||||
if (box.entry_history_pos == -1)
|
|
||||||
{
|
|
||||||
box.entry_history_pos = box.entry_history.length - 1;
|
|
||||||
}
|
|
||||||
else if (box.entry_history_pos > 0)
|
|
||||||
{
|
|
||||||
box.entry_history_pos -= 1;
|
|
||||||
}
|
|
||||||
box.value = box.entry_history[box.entry_history_pos];
|
|
||||||
}
|
|
||||||
else if (event.keyCode == 27)
|
|
||||||
{
|
|
||||||
box.value = "";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
box.entry_history_pos = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 448 B |
|
|
@ -1,5 +0,0 @@
|
||||||
{% macro make_header() %}
|
|
||||||
<div id="header">
|
|
||||||
<a class="header_element" href="/">Home</a>
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<!DOCTYPE html5>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
{% import "header.html" as header %}
|
|
||||||
<title>Flasksite</title>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link rel="stylesheet" href="/static/common.css">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body, a
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{header.make_header()}}
|
|
||||||
<p>Welcome to my flask site</p>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
</script>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<!DOCTYPE html5>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
{% import "header.html" as header %}
|
|
||||||
<title>Flasksite</title>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link rel="stylesheet" href="/static/common.css">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="content_body">
|
|
||||||
<p>test</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
</script>
|
|
||||||
</html>
|
|
||||||
|
Before Width: | Height: | Size: 267 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 107 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 131 B |
|
|
@ -298,10 +298,12 @@ def handle_vidme(url, customname=None):
|
||||||
pagedata = pagedata[0]
|
pagedata = pagedata[0]
|
||||||
pagedata = pagedata.split('content="')[1].split('"')[0]
|
pagedata = pagedata.split('content="')[1].split('"')[0]
|
||||||
pagedata = pagedata.replace('&', '&')
|
pagedata = pagedata.replace('&', '&')
|
||||||
headers = {'Referer': 'https://vid.me/',
|
headers = {
|
||||||
'Range':'bytes=0-',
|
'Referer': 'https://vid.me/',
|
||||||
'Host':'d1wst0behutosd.cloudfront.net',
|
'Range':'bytes=0-',
|
||||||
'Cache-Control':'max-age=0'}
|
'Host':'d1wst0behutosd.cloudfront.net',
|
||||||
|
'Cache-Control':'max-age=0'
|
||||||
|
}
|
||||||
|
|
||||||
return download_file(pagedata, customname, headers=headers)
|
return download_file(pagedata, customname, headers=headers)
|
||||||
|
|
||||||
|
|
@ -387,7 +389,7 @@ HANDLERS = {
|
||||||
'youtube.com': handle_youtube,
|
'youtube.com': handle_youtube,
|
||||||
'youtu.be': handle_youtube,
|
'youtu.be': handle_youtube,
|
||||||
'twitter.com': handle_twitter
|
'twitter.com': handle_twitter
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_master(url, customname=None):
|
def handle_master(url, customname=None):
|
||||||
print('Handling %s' % url)
|
print('Handling %s' % url)
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 168 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 168 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 168 B After Width: | Height: | Size: 128 B |