diff --git a/pub/.~tmp~ b/pub/.~tmp~ new file mode 100644 index 0000000..e69de29 diff --git a/scripts/GeoLite2-Country.mmdb b/scripts/GeoLite2-Country.mmdb new file mode 100644 index 0000000..c7e7abb Binary files /dev/null and b/scripts/GeoLite2-Country.mmdb differ diff --git a/scripts/all.sh b/scripts/all.sh new file mode 100755 index 0000000..9b3fa40 --- /dev/null +++ b/scripts/all.sh @@ -0,0 +1,12 @@ +/srv/mirror/scripts/sync.sh archlinux & +/srv/mirror/scripts/sync.sh fedora & +/srv/mirror/scripts/sync.sh debian & +/srv/mirror/scripts/sync.sh ubuntu & +/srv/mirror/scripts/sync.sh kali & +/srv/mirror/scripts/sync.sh manjaro & +/srv/mirror/scripts/sync.sh raspbian & +/srv/mirror/scripts/sync.sh ubuntu_cd & +/srv/mirror/scripts/sync.sh kali_images & +/srv/mirror/scripts/sync.sh debian_cd & +/srv/mirror/scripts/sync.sh linux & +/srv/mirror/scripts/sync.sh gnu & \ No newline at end of file diff --git a/scripts/base.html b/scripts/base.html new file mode 100644 index 0000000..401f484 --- /dev/null +++ b/scripts/base.html @@ -0,0 +1,214 @@ + + + + + + Morgan's Mirror + + + + + + + + +
+

Welcome to Morgan's mirror archive!

+

Simple archiving server.

+

This mirror is for archiving and NOT for public serving.

+
+
+
+

Archive List

+
+ +
+

ArchLinux (x86_64)

+

Last Updated: @@archlinux@@

+

Source: rsync://mirrors.xtom.de/archlinux/

+
+ +
+

Ubuntu

+

Last Updated: @@ubuntu@@

+

Source: rsync://rsync.archive.ubuntu.com/ubuntu/

+
+ +
+

Ubuntu Releases

+

Last Updated: @@ubuntu_cd@@

+

Source: rsync://releases.ubuntu.com/releases/

+
+ +
+

Debian

+

Last Updated: @@debian@@

+

Source: rsync://ftp.halifax.rwth-aachen.de/debian/

+
+ +
+

Debian Releases

+

Last Updated: @@debian_cd@@

+

Source: rsync://ftp.lanet.kr/debian-cd/

+
+ +
+

Kali Linux

+

Last Updated: @@kali@@

+

Source: rsync://archive.kali.org/

+
+ +
+

Kali Linux Images

+

Last Updated: @@kali_images@@

+

Source: rsync://archive.kali.org/

+
+ +
+

Manjaro

+

Last Updated: @@manjaro@@

+

Source: rsync://ftp.riken.jp/manjaro/

+
+ +
+

Raspbian

+

Last Updated: @@raspbian@@

+

Source: rsync://archive.raspbian.org/archive/

+
+ +
+

Fedora

+

Last Updated: @@fedora@@

+

Source: rsync://dl.fedoraproject.org/fedora-enchilada/linux/

+
+ +
+

Ubuntu Releases (Old)

+

Last Updated: @@ubuntu_cd_old@@

+

Source: rsync://old-releases.ubuntu.com/releases/

+
+ +
+ +
+

GNU

+

Last Updated: @@gnu@@

+

Source: rsync://ftp.gnu.org/gnu/

+
+ +
+

Linux Kernel

+

Last Updated: @@linux@@

+

Source: rsync://kernel.org

+
+ + + +
+
+
+

Server Details

+

Location: Seoul, Korea

+

Bandwidth: 100Mbps

+

Storage size: 16TiB

+

Sync delay: 4hrs

+

Contact: mirror@morgan.kr

+
+ +
+ + + + + + diff --git a/scripts/config.yml b/scripts/config.yml deleted file mode 100644 index a2ce795..0000000 --- a/scripts/config.yml +++ /dev/null @@ -1,131 +0,0 @@ -BASE_DIR: /srv/mirror - -# Any path under this must be in relative of BASE_DIR. -# Script doesnt do any kind of outbound check, so make sure to check all paths. - -global: - scripts: - pre: - post: - log: logs/sync.log - log_dir: logs/ - data_dir: pub/ - sync: - - ALL - -defaults: - type: rsync - scripts: - pre: - fail: - - scripts/alert.sh - post: - - scripts/index.py ${config} - log: logs/${path}.log - delay: 4 - rsync: - options: "-rtlHpv --chmod=D0755,F0644 --partial --hard-links --safe-links --stats --delete --delete-after --delay-updates --max-delete=70000" - exclude: - - ".*.??????" - - ".~tmp~/" - - "Packages*" - - "Sources*" - - "Release*" - - "*.links.tar.gz*" - - "/other" - - "/sources" - -repos: - ARCHLINUX: - url: rsync://mirrors.xtom.de/archlinux/ - name: "ArchLinux (x86_64)" - path: archlinux - - UBUNTU: - url: rsync://rsync.archive.ubuntu.com/ubuntu/ - name: "Ubuntu" - path: ubuntu - - UBUNTU_CD: - url: rsync://releases.ubuntu.com/releases/ - name: "Ubuntu Releases" - delay: 6 - path: ubuntu_cd - - UBUNTU_CD_OLD: - url: rsync://old-releases.ubuntu.com/releases/ - name: "Ubuntu Releases (Old)" - delay: 12 - path: ubuntu_cd_old - - DEBIAN: - url: rsync://mirrors.xtom.jp/ - type: ftpsync - name: Debian - path: debian - scripts: - - # pre: scripts/pre_debian.sh - last_sync: "2023-10-12T10:58Z" - - DEBIAN_CD: - url: rsync://ftp.lanet.kr/debian-cd/ - delay: 6 - name: "Debian CD" - path: debian_cd - - FEDORA: - url: rsync://dl.fedoraproject.org/fedora-enchilada/linux/ - name: "Fedora" - path: fedora - - RASPBIAN: - url: rsync://archive.raspbian.org/archive/ - delay: 6 - name: "Raspbian" - path: raspbian - - MANJARO: - url: rsync://ftp.riken.jp/manjaro/ - name: Manjaro - path: manjaro - - ARCHLINUXARM: - name: "ArchLinux (ARM)" - path: archlinuxarm - url: http://jp.mirror.archlinuxarm.org/ - type: http - delay: 12 - - ASAHILINUX: - name: "AsahiLinux" - path: asahilinux - url: https://cdn.asahilinux.org/ - type: http - delay: 12 - - GNU: - name: "GNU" - path: gnu - url: rsync://ftp.gnu.org/gnu/ - delay: 12 - - LINUX: - name: "Linux Kernel" - path: linux - url: rsync://rsync.kernel.org/pub/ - delay: 12 - -index: - - ARCHLINUX - - UBUNTU UBUNTU_CD - - DEBIAN DEBIAN_CD - - FEDORA - - MANJARO - - RASPBIAN - - DIVIDER - - ARCHLINUXARM - - ASAHILINUX - - GNU - - LINUX - - UBUNTU_CD_OLD \ No newline at end of file diff --git a/scripts/ftpsync.conf b/scripts/ftpsync.conf index 7008b3f..2fde1b2 100644 --- a/scripts/ftpsync.conf +++ b/scripts/ftpsync.conf @@ -1,12 +1,50 @@ -MIRRORNAME="${DEB_HOST}" -TO="${DEB_PATH}" +######################################################################## +######################################################################## +## This is a sample configuration file for the ftpsync mirror script. ## +## Only options most users may need are included. For documentation ## +## and all available options see ftpsync.conf(5). ## +######################################################################## +######################################################################## + +echo ${BASE_DIR} +echo ${DATA_DIR} + +MIRRORNAME="mirror.morgan.kr" +TO="${DATA_DIR}/debian" +# MAILTO="$LOGNAME" HUB=false -RSYNC_HOST="${DEB_UPSTREAM}" + +######################################################################## +## Connection options +######################################################################## + +RSYNC_HOST="mirrors.xtom.jp" RSYNC_PATH="debian" -INFO_MAINTAINER="${DEB_ADMIN}" -INFO_COUNTRY="${DEB_COUNTRY}" -INFO_LOCATION="${DEB_LOCATION}" -INFO_THROUGHPUT=1Gbps -LOGDIR="${DEB_LOG}" +# RSYNC_USER= +# RSYNC_PASSWORD= + +######################################################################## +## Mirror information options +######################################################################## + +INFO_MAINTAINER="Morgan " +# INFO_SPONSOR="Example " +INFO_COUNTRY=KR +INFO_LOCATION="Seoul, Korea" +INFO_THROUGHPUT=1Gb + +######################################################################## +## Include and exclude options +######################################################################## + +# ARCH_INCLUDE= +# ARCH_EXCLUDE= + +######################################################################## +## Log option +######################################################################## + +LOGDIR="${BASE_DIR}/logs" + UIPRETRIES=9 -TRACEHOST="${DEB_HOST}" \ No newline at end of file +TRACEHOST="mirror.morgan.kr" diff --git a/scripts/getFetch.py b/scripts/getFetch.py deleted file mode 100644 index 4360d84..0000000 --- a/scripts/getFetch.py +++ /dev/null @@ -1,81 +0,0 @@ -import requests, json, sys -from bs4 import BeautifulSoup as bs4 - -def tree(idx: int, flag = False) -> str: - idx += 1 - if idx == 1: - if flag : - return "├────" - return "├──" - elif idx == 2: - if flag : - return "│ ├────" - return "│ ├──" - else: - if flag : - return "│ " * (idx - 2) + "├────" - return "│ " * (idx - 2) + "├──" - -def parseIndex(url, base = "", idx = 0, dirs = [], ret = []): - - print(f"D {(tree(idx)+url):<40} {''.join(dirs):>80}") - html = bs4(requests.get(base + url).text, features="html.parser") - - hrefs = [a["href"] for a in html.find_all('a')] - hrefs = [i for i in hrefs if i[0] != '?'] - fls = [] - for href in hrefs: - if href[-1] == "/": # if dir - if href[0] != "/" and href[0] != ".": - parseIndex(href, base + url, idx + 1, dirs + [href], ret) - else: - if href[0:2] == "./" and "/" not in href[2:]: - href = href[2:] - assert "/" not in href - print(f"F {(tree(idx, 1)+href):<80}") # if file - fls.append(href) - - while len(ret) <= idx: - ret.append({}) - - if ''.join(dirs) not in ret[idx].keys(): - ret[idx][''.join(dirs)] = [] - - for fl in fls: - ret[idx][''.join(dirs)].append({fl: base + url + fl}) - - return ret - - -if __name__ == "__main__": - - if len(sys.argv) != 3 and len(sys.argv) != 4: - print("Usage: createFetch.py [URL] [Path] (fetch)") - sys.exit() - - if "http" in sys.argv[1]: - if input(f"[*] Download from {sys.argv[1]}? ") in "Yy": - url = sys.argv[1] - else: - sys.exit() - else: - print("[*] Not supported") - sys.exit() - - bpath = sys.argv[2] - assert(bpath[-1] == '/') - - print() - print(f"[*] Downloading File list from {url} with base path {bpath}") - - files = parseIndex('', base = url, dirs = [bpath]) - filename = url.split('/')[2]+'.fetch' - - if len(sys.argv) == 4: - filename = sys.argv[3] - - with open(filename, 'w') as f: - f.write(json.dumps(files, indent=4)) - - print() - print(f"[*] Saved to {filename}.") \ No newline at end of file diff --git a/scripts/getFile.py b/scripts/getFile.py deleted file mode 100644 index 80650ac..0000000 --- a/scripts/getFile.py +++ /dev/null @@ -1,114 +0,0 @@ -# import asyncio, aiohttp, aiofiles -import json, os, sys -import requests, time -# from tqdm import tqdm - -def byteSize(size): - pr = "" - if size > 1024: - size = round(size / 1024, 1) - pr = "K" - if size > 1024: - size = round(size / 1024, 1) - pr = "M" - if size > 1024: - size = round(size / 1024, 1) - pr = "G" - return f"{size}{pr}" - -if len(sys.argv) != 2: - print("Usage: getFiles.py [fetch file]") - sys.exit() - -listf = sys.argv[1] -if not os.path.exists(listf): - print("There is no such file..") - sys.exit() - -print(f"Downloading from fetchFile {listf}") - -def spchr(string, num = 25, pad = 0): - if len(string) > num: - string = string[0:20] + ".." + string[-4:] - if pad > num: - string = string + ' '*(pad-len(string)) - return string - -with open(listf, 'r') as f: - jstr = json.loads(f.read()) - -for stage in jstr: - for redirs in stage.keys(): - der = redirs.split('/') - for i in range(len(der)): - dpath = '/'.join(der[0:i]) - if not os.path.exists(dpath) and dpath: - print(f"[*] Making new directory {dpath}") - os.mkdir(dpath) - -for stage in jstr: - for dosta in stage: - print(f"[*] Downloading path {dosta}") - fpaths = [] - furls = [] - - for fls in stage[dosta]: - fna = list(fls.keys())[0] - fpa = fls[fna] - fpaths.append(dosta + fna) - furls.append(fpa) - - assert len(fpaths) == len(furls) - - # pbar = tqdm(total=len(fpaths)) - # print(furls) - dlist = [] - for url, path in zip(furls, fpaths): - if os.path.exists(path): - flen = os.path.getsize(path) - wlen = requests.get(url, stream=True).headers['Content-length'] - if int(flen) != int(wlen): - dlist.append((path, url)) - else: - dlist.append((path, url)) - - for path, url in dlist: - print(f"[*] Fetch {url} ", end = "") - wfil = requests.get(url, stream=True) - wlen = wfil.headers['Content-length'] - print(byteSize(int(wlen))) - - with open(path, 'wb') as f: - if wlen is None: # no content length header - f.write(wfil.content) - - else: - # pbar = tqdm(total=int(wlen), desc=spchr(' '+path, pad = 30)) - for data in wfil.iter_content(chunk_size=4096): - f.write(data) - # print(".", end = "") - # pbar.update(len(data)) - # print() -# async def getHTTP(session, url): -# async with session.get(url) as resp: -# try: -# reqa = await resp.read() -# # pbar.update(1) -# return reqa -# except: -# print(url) -# return - -# async def agetReq(): -# async with aiohttp.ClientSession() as session: -# tasks = [] -# for url in furls: -# tasks.append(asyncio.ensure_future(getHTTP(session, url))) - -# getReqs = await asyncio.gather(*tasks) - -# for i, getReq in enumerate(getReqs): -# async with aiofiles.open(fpaths[i], mode='wb') as handle: -# await handle.write(getReq) - -# asyncio.run(agetReq()) diff --git a/scripts/index.py b/scripts/index.py index 94d40e8..9838f53 100755 --- a/scripts/index.py +++ b/scripts/index.py @@ -1,86 +1,67 @@ -#!/usr/bin/python3 +#!/usr/bin/python -import os, sys -import yaml -import jinja2 +import os, sys, re import datetime -# def get_last_sync(repo_name): -# if os.path.exists(SYNC_LOG): -# with open(SYNC_LOG, 'r') as log_file: -# log_all = reversed(log_file.readlines()) -# for logline in log_all: -# dist, time, stat = logline.split() -# if repo_name == dist and stat =="SUCCESS": -# return datetime.datetime.strptime(time, '%Y-%m-%dT%H:%MZ').strftime("%Y-%m-%d %H:%M") -# return "Not Synced" +base_path = sys.argv[1] +out_path = sys.argv[2] +assert os.path.exists(base_path) -# main() +log_path = os.path.join(base_path, "logs/all.log") +assert os.path.exists(log_path) -if __name__=="__main__": +html_path = os.path.join(base_path, "scripts/base.html") +index = os.path.join(out_path, "index.html") +assert os.path.exists(html_path) - if len(sys,argv) == 3: - print(sys.argv) +with open(log_path, 'r') as f: + log_file = f.read().splitlines() +log_file.reverse() - CONFIG_PATH = sys.argv[1] +with open(html_path, 'r') as f: + html_file = f.read() +dists = re.findall("@@([^@@]+)@@", html_file) - with open(CONFIG_PATH, 'r') as f: - config = yaml.safe_load(f) +print(dists) +logs = {} +for dist in dists: + logs[dist] = [] - BASE_DIR = config.get("BASE_DIR", ".") - THEME = "new" - TEMPLATES_DIR = os.path.join(os.path.join(BASE_DIR, "scripts/templates"), THEME) - LOG_DIR = os.path.join(BASE_DIR, config['global'].get("log_dir", "logs")) - OUTPUT_PATH = os.path.join(os.path.join(BASE_DIR, config['global'].get("data_dir", ".")), "index.html") +for dist in logs: + for logline in log_file: + time, stat, disk = logline.split(" ") + if stat == "DONE" and dist == disk: + logs[dist] = datetime.datetime.strptime(time, '%Y%m%d_%H%M').strftime("%Y-%m-%d %H:%M") + break + logs[dist] = "Not Synced" - env = jinja2.Environment(loader=jinja2.FileSystemLoader(TEMPLATES_DIR)) - base_template = env.get_template('base.html') - full_template = env.get_template('full.html') - half_template = env.get_template('half.html') +stats = {} +for dist in dists: + for logline in log_file: + time, stat, disk = logline.split(" ") + if dist == disk: + stats[dist] = stat, datetime.datetime.strptime(time, '%Y%m%d_%H%M').strftime("%Y-%m-%d %H:%M") + break + stats[dist] = (-1, "Not Synced") - main_repos = [] - additional_repos = [] +print(logs) +print(stats) - DIV = 0 - for line in config['index']: - if line == "DIVIDER": - DIV = 1 - continue - repos_line = line.split() - for repo_name in repos_line: - repo_data = config['repos'].get(repo_name) - if not repo_data: - continue +for dist in dists: + stat, tt = stats[dist] + if stat == "ERROR": + stat = f" (Error @ {tt})" + elif stat == "STARTED": + stat = f" (Running @ {tt})" + else: + stat = "" + + if stat: + stat = f"

{stat}

" + html_file = html_file.replace(f"@@{dist}@@", f"{logs[dist]}{stat}") - context = { - 'path': repo_data['path'], - 'name': repo_data['name'], - # 'lastsync': get_last_sync(repo_name), - 'lastsync': repo_data.get('lastsync', "Not Synced"), - 'upstream': repo_data['url'] - } - print(context) - if len(repos_line) > 1: - (main_repos if not DIV else additional_repos).append(half_template.render(**context)) - else: - (main_repos if not DIV else additional_repos).append(full_template.render(**context)) +with open(index, 'w') as f: + f.write(html_file) - html_output = base_template.render( - repos="\n".join(main_repos), - repos_more="\n".join(additional_repos) - ) - - try: - with open(OUTPUT_PATH, 'w') as f: - f.write(html_output) - - except: - if len(sys.argv) == 2: - if os.path.exists(sys.argv[1]): - if not os.path.isdir(sys.argv[1]): - print(f"Writing to {sys.argv[1]}") - with open(sys.argv[1], 'w') as f: - f.write(html_output) - else: - print(html_output) +print("Written to index.html") diff --git a/scripts/nginx/nginx.conf b/scripts/nginx/nginx.conf deleted file mode 100644 index 2661e28..0000000 --- a/scripts/nginx/nginx.conf +++ /dev/null @@ -1,91 +0,0 @@ -user user; -worker_processes auto; -pid /run/nginx.pid; -include /etc/nginx/modules-enabled/*.conf; - -events { - worker_connections 768; - # multi_accept on; -} - -http { - - ## - # Basic Settings - ## - - sendfile on; - tcp_nopush on; - types_hash_max_size 2048; - # server_tokens off; - - # server_names_hash_bucket_size 64; - # server_name_in_redirect off; - - include /etc/nginx/mime.types; - default_type application/octet-stream; - - ## - # SSL Settings - ## - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE - ssl_prefer_server_ciphers on; - - ## - # Logging Settings - ## - - log_format main_ext '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for" ' - '"$host" sn="$server_name" ' 'rt=$request_time ' - 'ua="$upstream_addr" us="$upstream_status" ' - 'ut="$upstream_response_time" ul="$upstream_response_length" ' - 'cs=$upstream_cache_status' ; - - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - - ## - # Gzip Settings - ## - - gzip on; - - # gzip_vary on; - # gzip_proxied any; - # gzip_comp_level 6; - # gzip_buffers 16 8k; - # gzip_http_version 1.1; - # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; - - ## - # Virtual Host Configs - ## - - include /etc/nginx/conf.d/*.conf; - include /etc/nginx/sites-enabled/*; -} - - -#mail { -# # See sample authentication script at: -# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript -# -# # auth_http localhost/auth.php; -# # pop3_capabilities "TOP" "USER"; -# # imap_capabilities "IMAP4rev1" "UIDPLUS"; -# -# server { -# listen localhost:110; -# protocol pop3; -# proxy on; -# } -# -# server { -# listen localhost:143; -# protocol imap; -# proxy on; -# } -#} diff --git a/scripts/nginx/pawe.me.conf b/scripts/nginx/pawe.me.conf deleted file mode 100644 index a75fc04..0000000 --- a/scripts/nginx/pawe.me.conf +++ /dev/null @@ -1,21 +0,0 @@ -server { - listen 80 ; - listen [::]:80 ; - server_name repo.city; - - access_log /var/log/nginx/repo-city-access.log main_ext; - error_log /var/log/nginx/repo-city-error.log warn; - - root /srv/mirror; - - location /scripts/ { - return 404; - } - - location / { - autoindex on; - fancyindex on; - } -} - - diff --git a/scripts/nginx/stub_status.conf b/scripts/nginx/stub_status.conf deleted file mode 100644 index f262466..0000000 --- a/scripts/nginx/stub_status.conf +++ /dev/null @@ -1,11 +0,0 @@ -server { - listen 127.0.0.1:80; - server_name 127.0.0.1; - location /nginx_status { - stub_status on; - access_log off; - allow 127.0.0.1; - deny all; - } -} - diff --git a/scripts/ngparse b/scripts/ngparse new file mode 100755 index 0000000..f03583d --- /dev/null +++ b/scripts/ngparse @@ -0,0 +1,230 @@ +#!/usr/bin/python3 + +import re, math, os, sys, datetime + +path_list = ["archlinux", "/archlinuxarm", "/asahilinux", + "/cd-image", "/debian", "/debian-cd", "/fedora", + "/gnu", "/index.html", "/kali", "/kali-images", + "/linux", "/manjaro", "/raspbian", "/static", + "/ubuntu", "/ubuntu-cd", "/ubuntu-old", "/"] + +def byte_human(size_bytes): + if size_bytes == 0: + return "0B" + size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") + i = int(math.floor(math.log(size_bytes, 1024))) + p = math.pow(1024, i) + s = round(size_bytes / p, 2) + return "%s%s" % (s, size_name[i]) + + +def parse_req_line(http_entry): + req = {} + http_parsed = http_entry.split() + if http_parsed: + if http_parsed[0] in ["HEAD", "POST", "GET", "OPTION"]: + req["method"] = http_parsed[0] + if http_parsed[1][0] == "/": + req["path"] = http_parsed[1] + return req + + +def get_path_parent(path): + path = path.split("?")[0].split("&")[0].split("/") + if len(path) == 1: + return "/" + path[0] + else: + return "/".join(path[:2]) + +def get_date_range(all_log): + dates = sorted([ i[2] for i in all_log ]) + dates = [datetime.datetime.strptime(i, "%d/%b/%Y:%H:%M:%S %z").strftime("%m/%d %H:%M:%S") for i in dates] + print(f"Date: {dates[0]} ~ {dates[-1]}") + + +def parse_log_entry(entry): + pattern = r'([\d\.]+) - (\S+) \[(.*?)\] "(.*?)" (\d+) (\d+) "(.*?)" "(.*?)" "(.*?)" "(.*?)" sn="(.*?)" rt=([\d\.]+) [^\n]+' + + match = re.match(pattern, entry) + + if not match: + return None + + lm = list(match.groups()) + assert len(lm) == 12 + + if "HTTP" not in lm[3]: + return + + log_entry = {} + log_entry["client"] = lm[0] + log_entry["user"] = lm[1] + log_entry["time"] = datetime.datetime.strptime(lm[2], "%d/%b/%Y:%H:%M:%S %z") + log_entry["req"] = lm[3] + log_entry["status"] = lm[4] + log_entry["bytes"] = lm[5] + log_entry["referer"] = lm[6] + log_entry["ua"] = lm[7] + log_entry["forward"] = lm[8] + log_entry["host"] = lm[9] + log_entry["server"] = lm[10] + log_entry["reqtime"] = lm[11] + + return log_entry + + +def get_all_log_entry(log_file): + with open(log_file, "r") as f: + full_log = f.read().split("\n") + log_entries = [ i for i in [ parse_log_entry(log) for log in full_log[:-1] ] if i ] + return log_entries + + +def main(log_file, logs=[]): + all_logs = get_all_log_entry(log_file) if not logs else logs + + log_by_date = {} + for entry in all_logs: + date_day = datetime.datetime.strftime(entry.get("time"), "%Y-%m-%d") + if date_day not in log_by_date: + log_by_date[date_day] = [] + log_by_date[date_day].append(entry) + + for day in log_by_date: + paths = {} + for entry in log_by_date[day]: + req = parse_req_line(entry.get("req")) + + if path := req.get("path"): + path_parent = get_path_parent(path) + + if path_parent not in path_list: + continue + + if path_parent not in paths: + paths[path_parent] = [0, 0] + + paths[path_parent][0] += int(entry.get("bytes")) + paths[path_parent][1] += 1 + + print(day) + print('-'*36) + print(f'{"Path":<14} {"Count":<10} Transfer') + print('-'*36) + for path in sorted(paths): + print(f"{path:<14} {paths[path][1]:<10} {byte_human(paths[path][0])}") + print() + + +def main_geo(log_file, logs = []): + import geoip2.database + reader = geoip2.database.Reader('GeoLite2-Country.mmdb') + + all_logs = get_all_log_entry(log_file) if not logs else logs + c = len(all_logs) + # print(c) + geolocstat = {} + for n, log in enumerate(all_logs): + req = parse_req_line(log.get("req")) + if path := req.get("path"): + path_parent = get_path_parent(path) + + try: + geoloc = reader.country(log['client']).country.iso_code + except: + geoloc = "XX" + + print(f"[{int(100*n/c):>3}%] {geoloc} {log['client']:>15} {byte_human(int(log['bytes'])):>8} {path_parent}", file=sys.stderr) + if geoloc not in geolocstat: + geolocstat[geoloc] = 0 + geolocstat[geoloc] += int(log['bytes']) + print(file=sys.stderr) + + sortdict = lambda x : {k: v for k, v in sorted(x.items(), key=lambda item: item[1], reverse=True)} + for geo in sortdict(geolocstat): + print(geo, byte_human(geolocstat[geo])) + +def main_date(log_file, logs=[]): + all_logs = get_all_log_entry(log_file) if not logs else logs + date_sorted = (sorted(all_logs, key=(lambda x: x['time']))) + st = datetime.datetime.strftime(date_sorted[0]['time'], "%Y-%m-%d %H:%M:%S") + ed = datetime.datetime.strftime(date_sorted[-1]['time'], "%Y-%m-%d %H:%M:%S") + total_bytes = sum([int(x['bytes']) for x in all_logs]) + print(f"------- Log {log_file} -------\n Date: {st} ~ {ed}\n Entry count: {len(all_logs)}\n Total bytes: {byte_human(total_bytes)}") + +def main_html(log_file): + all_logs = get_all_log_entry(log_file) +### + date_sorted = (sorted(all_logs, key=(lambda x: x['time']))) + st = datetime.datetime.strftime(date_sorted[0]['time'], "%Y-%m-%d %H:%M:%S") + ed = datetime.datetime.strftime(date_sorted[-1]['time'], "%Y-%m-%d %H:%M:%S") + total_bytes = sum([int(x['bytes']) for x in all_logs]) + print(f"-------- Log Info --------\nDate: {st} ~ {ed}\nEntry count: {len(all_logs)}\nTotal bytes: {byte_human(total_bytes)}\n") +### + print("-------- By Path ---------") + + paths = {} + for entry in all_logs: + req = parse_req_line(entry.get("req")) + + if path := req.get("path"): + path_parent = get_path_parent(path) + + if path_parent not in path_list: + continue + + if path_parent not in paths: + paths[path_parent] = [0, 0] + + paths[path_parent][0] += int(entry.get("bytes")) + paths[path_parent][1] += 1 + + for path in sorted(paths): + print(f"{path:<14} {paths[path][1]:<10} {byte_human(paths[path][0])}") + print() +### + print("------- By Country -------") + import geoip2.database + reader = geoip2.database.Reader('/srv/mirror/scripts/GeoLite2-Country.mmdb') + + geolocstat = {} + for n, log in enumerate(all_logs): + req = parse_req_line(log.get("req")) + if path := req.get("path"): + path_parent = get_path_parent(path) + try: + geoloc = reader.country(log['client']).country.iso_code + except: + geoloc = "XX" + if geoloc not in geolocstat: + geolocstat[geoloc] = 0 + geolocstat[geoloc] += int(log['bytes']) + + geos = [] + sortdict = lambda x : {k: v for k, v in sorted(x.items(), key=lambda item: item[1], reverse=True)} + for geo in sortdict(geolocstat): + if geo: + geos.append(f"{geo} {byte_human(geolocstat[geo])}") + print('\n'.join(geos[:min(len(geos),7)])) + print("--------------------------") + +if __name__=="__main__": + if len(sys.argv) != 3: + print("Error: ngparse {stat,parse,geo} [ log file ]\n* log file must have extended format.") + exit() + + if os.path.exists(sys.argv[2]): + logfile = sys.argv[2] + else: + print("Error: File doesnt exists.") + exit + + if sys.argv[1] == "stat": + main(logfile) + elif sys.argv[1] == "geo": + main_geo(logfile) + elif sys.argv[1] == "date": + main_date(logfile) + elif sys.argv[1] == "html": + main_html(logfile) + diff --git a/static/robots.txt b/scripts/robots.txt similarity index 100% rename from static/robots.txt rename to scripts/robots.txt diff --git a/scripts/stat.sh b/scripts/stat.sh new file mode 100755 index 0000000..9a43efb --- /dev/null +++ b/scripts/stat.sh @@ -0,0 +1,18 @@ +#!/bin/bash +echo "

$(date +%Y%m%d)

Log

"> /srv/mirror/pub/stat/stat.html +echo "
$(/srv/mirror/scripts/ngparse html /var/log/nginx/mirror/access.log)
" >> /srv/mirror/pub/stat/stat.html +echo "

Traffic

" >> /srv/mirror/pub/stat/stat.html +echo "
$(vnstat -i enp1s0 --days 10 | sed 's/    //' | tail -n +4)
" >> /srv/mirror/pub/stat/stat.html + +for i in /var/log/nginx/mirror/access.log-*; +do + DATE=${i#*-} + echo $DATE + if [ ! -f /srv/mirror/pub/stat/$DATE.html ]; + then + echo $DATE.html; + echo "$DATE" > /srv/mirror/pub/stat/$DATE.html; + echo "
$(/srv/mirror/scripts/ngparse html $i)
" >> /srv/mirror/pub/stat/$DATE.html + echo "

" >> /srv/mirror/pub/stat/$DATE.html + fi +done \ No newline at end of file diff --git a/scripts/sync.sh b/scripts/sync.sh index b38073f..63a4676 100755 --- a/scripts/sync.sh +++ b/scripts/sync.sh @@ -1,301 +1,110 @@ #!/bin/bash -# Default locations. -CONFIG="./scripts/config.yml" -YAML="./scripts/yq" +TIMENOW=$(date '+%Y%m%d_%H%M') +BASE_DIR="/srv/mirror" +ALERT="$BASE_DIR/scripts/alert.sh" +DATA_DIR="$BASE_DIR/pub" -function usage() { - cat <v4.35.2) - -v, --verbose - -> -b, --base-dir: overrides base_dir - -> -d, --dry-run: dry run -EOF - exit 1 -} +if [[ "$USER" == "root" ]]; then + echo "Dont run as root." + su user $0 $@ + exit +fi -function error() { - echo $0: "$1" - exit -} +option="-rtlHpvi --chmod=D0755,F0644 --partial --hard-links --safe-links --stats --delete --delete-after --delay-updates --max-delete=70000" +exclude="--exclude=.*.?????? --exclude='.~tmp~/' --exclude='Packages*' --exclude='Sources*' --exclude='Release*' --exclude='*.links.tar.gz*' --exclude='/other' --exclude='/sources'" -function debug() { - if [[ -v DEBUG ]]; then - echo debug: "$1" - fi -} +ubuntu="rsync://rsync.archive.ubuntu.com/ubuntu/" +ubuntu_cd="rsync://releases.ubuntu.com/releases/" +ubuntu_cd_old="rsync://old-releases.ubuntu.com/releases/" +debian="rsync://mirrors.xtom.jp/debian/" +debian_cd="rsync://ftp.lanet.kr/debian-cd/" +fedora="rsync://dl.fedoraproject.org/fedora-enchilada/linux/" +epel="rsync://dl.fedoraproject.org/fedora-epel/" +fedora_cd="" +archlinux="rsync://mirrors.xtom.de/archlinux/" +raspbian="rsync://archive.raspbian.org/archive/" +manjaro="rsync://ftp.riken.jp/manjaro/" +gnu="rsync://ftp.gnu.org/gnu/" +kali_images="rsync://repo.jing.rocks/kali-images" +kali="rsync://repo.jing.rocks/kali" +linux="rsync://rsync.kernel.org/pub/" +failtest="rsync://aa" +if [[ ! -v $1 ]]; then + echo Not found. + exit +fi -function execute() { - debug "Executing script: \"$@\"" - if [[ ! -v DRY_RUN ]]; then - $@ - fi -} +ubuntu_cd_name="ubuntu-cd" +debian_cd_name="debian-cd" +ubuntu_cd_old_name="ubuntu-old" +kali_images_name="kali-images" -parse_yaml() { - echo $($YAML eval ".$1" "${CONFIG}") -} +dist=$1 +echo Syncing $1... +set -o pipefail +LASTLOG=`head -1 ${BASE_DIR}/logs/${dist}.log` -get_repo_config() { - local LOCAL_CFG=$($YAML eval ".repos.$1.$2" "${CONFIG}") - if [[ "$LOCAL_CFG" == "null" || "$LOCAL_CFG" == "" ]]; then - echo $($YAML eval ".defaults.$2" "${CONFIG}"); - return 1 - fi - echo $LOCAL_CFG - return 0 +mv ${BASE_DIR}/logs/${dist}.log ${BASE_DIR}/logs/previous/${dist}-${LASTLOG}.log +mv ${BASE_DIR}/logs/${dist}-error.log ${BASE_DIR}/logs/previous/${dist}-error-${LASTLOG}.log - # 1 if global 0 if local -} +if [[ -v ${dist}_name ]]; then + dist_name_var="${dist}_name" + dist_dir=${!dist_name_var} +else + dist_dir=$dist +fi -sync_repo() { - local repo=$1 - local name=$(get_repo_config ${repo} name) - local type=$(get_repo_config ${repo} type) - local url=$(get_repo_config ${repo} url) - local path=$(get_repo_config ${repo} path) - local log; log=$(get_repo_config ${repo} log) || log=${log/\${path\}/$path}; +echo ${TIMENOW} >> ${BASE_DIR}/logs/${dist}.log +echo ${TIMENOW} >> ${BASE_DIR}/logs/${dist}-error.log +echo "${TIMENOW}: Mirroring ${dist} from ${!dist} to ${DATA_DIR}/${dist}" +echo "${TIMENOW} STARTED ${dist}" >> ${BASE_DIR}/logs/all.log - if [[ "$type" == "null" || "$url" == "null" || "$path" == "null" || "$log" == "null" ]]; then - echo "Error config wrong." - return - fi - - full_path="${BASE_DIR}/$(parse_yaml global.data_dir)/$path/" - log="${BASE_DIR}/$log" - - echo -e "--------\nRepo: $name\nType: $type\nUpstream: $url\nPath: $full_path\nLog: $log\n--------" - - if [[ ! -v DRY_RUN ]]; then - - rotate_log $log - - case $type in - "rsync") - local rsync_options=$(get_repo_config $repo 'rsync.options') - local exclude_list=($(get_repo_config $repo 'rsync.exclude[]')) - local exclude="" - for ex in "${exclude_list[@]}"; do - exclude="${exclude} --exclude='${ex}'" - done - echo rsync ${rsync_options} ${exclude} $url $full_path >> $log - rsync ${rsync_options} ${exclude} $url $full_path >> $log 2>> ${log}-error - ;; - "ftpsync") - cd ${BASE_DIR}/scripts - - export DEB_ADMIN="Morgan " - export DEB_COUNTRY="Korea" - export DEB_LOCATION="Korea/Seoul" - export DEB_PATH="$full_path" - export DEB_HOST="$(parse_yaml global.hostname)" - export DEB_UPSTREAM="$url" - export DEB_LOG="${BASE_DIR}/$(parse_yaml global.log_dir)" - - ./ftpsync >> $log 2>> ${log}-error - cd ${BASE_DIR} - ;; - "http") - echo ${repo} Fetch >> $log 2>> ${log}-error - python3 -u $BASE_DIR/scripts/getFetch.py "${url}" $full_path $BASE_DIR/scripts/${path}.fetch >> $log 2>> ${log}-error - echo ${repo} Download >> $log 2>> ${log}-error - python3 -u $BASE_DIR/scripts/getFile.py $BASE_DIR/scripts/${path}.fetch >> $log 2>> ${log}-error - ;; - *) - echo "Unknown type $type for $repo." | tee ${log}-error - ;; - esac - - clean_log $log - fi -} - -rotate_log() { - local log_file=$1 - if [[ -f $log_file ]]; then - PREV_LOG=$(cat "$log_file" | head -n 1) - old_log_file="$(dirname $log_file)/old/$(basename $log_file)-$PREV_LOG" - mkdir -p "$(dirname $old_log_file)" - mv "$log_file" "$old_log_file" - fi - - local error_file=$1-error - if [[ -f $error_file ]]; then - PREV_LOG=$(cat "$error_file" | head -n 1) - old_error_file="$(dirname $error_file)/old/$(basename $error_file)-$PREV_LOG" - mkdir -p "$(dirname $old_error_file)" - mv "$error_file" "$old_error_file" - fi - - echo $TIMESTAMP >> $log_file - echo $TIMESTAMP >> $error_file -} - -clean_log() { - local error_file=$1-error - nl=$(cat "$error_file" | wc -l) - if [ $nl -eq 1 ]; then - rm "$error_file" - fi -} - -function global_log() { - GLOBAL_LOG_FILE="$(parse_yaml 'global.log')" - echo "$1 $TIMESTAMP SUCCESS" >> $GLOBAL_LOG_FILE -} - -function global_log_error() { - GLOBAL_LOG_FILE="$(parse_yaml 'global.log')" - echo "$1 $TIMESTAMP ERROR" >> $GLOBAL_LOG_FILE -} - -########## -# Main # -########## - -while [ "$1" != "" ]; do - case $1 in - -c | --config) - shift - if [[ -z $1 ]]; then - error "option '-c' requires argument 'config_path'" - fi - CONFIG=$1 - ;; - -y | --yq) - shift - if [[ -z $1 ]]; then - error "option '-y' requires argument 'yq_path'" - fi - YAML=$1 - ;; - -b | --base-dir) - shift - if [[ -z $1 ]]; then - error "option '-b' requires argument 'base_dir'" - fi - BASE_DIR_OVERRIDE=$1 - ;; - -h | --help) - usage - ;; - -v | --verbose) - DEBUG=1 - ;; - -d | --dry-run) - DRY_RUN=1 - ;; - *) - usage - exit 1 - ;; - esac - shift +TRY=3 +while [ $TRY -ne 0 ]; do + echo Try $TRY... + if [ "$dist" == "debian" ]; + then + cd ${BASE_DIR}/scripts + export BASE_DIR=${BASE_DIR} + export DATA_DIR=${DATA_DIR} + ./ftpsync + else + unset RSYNC_CONNECT_PROG + if [ "$dist" == "kali_images" ]; + then + # export RSYNC_CONNECT_PROG='ssh zhr0 nc %H 873' + echo Connecting to RSYNC PROG + fi + echo "rsync ${option} ${exclude} ${!dist} ${DATA_DIR}/${dist_dir}" | tee -a ${BASE_DIR}/logs/${dist}.log + rsync ${option} ${exclude} ${!dist} ${DATA_DIR}/${dist_dir} 2> >(tee -a ${BASE_DIR}/logs/${dist}-error.log) | tee -a ${BASE_DIR}/logs/${dist}.log + EXIT=$? + fi + if [[ $EXIT == 0 ]]; then break; fi + TRY=$(($TRY-1)) done -if [[ -v DEBUG ]]; then - debug "DEBUG=1" -fi -if [[ -v DRY_RUN ]]; then - debug "DRY_RUN=1" -fi -debug CONFIG="\"${CONFIG}\"" -debug YQ="\"${YAML}\"" - -if [[ ! -f ${CONFIG} ]]; then - error "config not found." +if [ $EXIT -ne 0 ]; +then + MSG="${dist} failed at ${TIMENOW}" + if [ -f "$ALERT" ]; + then + ${ALERT} "${MSG}" + fi + + echo Sync ${dist} Error + echo "${TIMENOW} ERROR ${dist}" >> ${BASE_DIR}/logs/all.log +else + echo Sync ${dist} Success + if [ `echo ${BASE_DIR}/logs/${dist}-error.log | wc -l` -eq 1 ]; + then + rm ${BASE_DIR}/logs/${dist}-error.log + fi + echo "${TIMENOW} DONE ${dist}" >> ${BASE_DIR}/logs/all.log + cd $BASE_DIR + echo "Updating Index" + python3 -u ./scripts/index.py ${BASE_DIR} ${DATA_DIR} fi -if [[ ! -f ${YAML} ]]; then - error "yq not found." -fi - -BASE_DIR=$($YAML eval ".BASE_DIR" "${CONFIG}") - -if [[ -v DEBUG && -v BASE_DIR_OVERRIDE ]]; then - debug "Overriding $BASE_DIR to $BASE_DIR_OVERRIDE" - BASE_DIR="$BASE_DIR_OVERRIDE" -fi - -TIMESTAMP=$(date '+%Y-%m-%dT%H:%MZ') - -debug BASE_DIR="\"${BASE_DIR}\"" -debug TIMESTAMP="\"${TIMESTAMP}\"" - -echo Started job $TIMESTAMP.. - -cd $BASE_DIR -# PRE -global_pre_scripts=($(parse_yaml 'global.scripts.pre[]')) -for script in "${global_pre_scripts[@]}"; do - execute $BASE_DIR/$script -done -# -repos=($(parse_yaml 'global.sync[]')) - -if [[ "${repos[0]}" == "ALL" ]]; then - repos=($($YAML eval '.repos | keys | .[]' "${CONFIG}")) -fi -for repo in "${repos[@]}"; do - cd $BASE_DIR - debug "Checking $repo..." - - delay=$(get_repo_config ${repo} "delay") - last_sync_timestamp=$(date -d "$(get_repo_config ${repo} "last_sync")" +%s) - next_sync_timestamp=$(( last_sync_timestamp + delay * 3600 )) - - if [[ -v DEBUG ]]; then - next_sync_timestamp=1 - # read -p "Continue? " choice - # case "$choice" in - # y) next_sync_timestamp=1;; - # *) continue;; - # esac - fi - - if [ $next_sync_timestamp -le $(date +%s) ]; then - debug "Lastsync was $last_sync_timestamp." - echo "Syncing $repo..." - - repo_pre_scripts=($(get_repo_config ${repo} "scripts.pre[]")) - for script in "${repo_pre_scripts[@]}"; do - execute $BASE_DIR/$script $repo - done - - sync_repo $repo - - if [ $? -ne 0 ]; then - repo_fail_scripts=($(get_repo_config ${repo} "scripts.fail[]")) - for script in "${repo_fail_scripts[@]}"; do - execute $BASE_DIR/$script $repo - done - - global_log_error $repo - echo "Error during syncing $repo." - else - - global_log $repo - $YAML eval ".repos.${repo}.last_sync = \"$TIMESTAMP\"" -i "${CONFIG}" - echo "Successfully synced $repo." - fi - - repo_post_scripts=($(get_repo_config ${repo} "scripts.post[]")) - for script in "${repo_post_scripts[@]}"; do - script=${script/\${config\}/$CONFIG} - execute $BASE_DIR/$script $repo - done - fi -done - -# POST -global_post_scripts=($(parse_yaml 'global.scripts.post[]')) -for script in "${global_post_scripts[@]}"; do - execute $BASE_DIR/$script -done -# - -echo Ended job $TIMESTAMP.. \ No newline at end of file diff --git a/scripts/templates/default/base.html b/scripts/templates/default/base.html deleted file mode 100644 index 0451d87..0000000 --- a/scripts/templates/default/base.html +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - PAWE.ME - - - - - - - - -
-

PAWE.ME

-

Simple mirroring & archiving server.

- -
-
-
-

Mirroring List

-
- {{ repos }} -
-
-
-

Additional Mirror

- -
- {{ repos_more }} - - - -
-
-
-

Server Info

-
    -
  • Xeon CPU
  • -
  • 64GiB Memory
  • -
  • 1Gbps Network, Seoul, Korea
  • -
  • 32TB Storage
  • -
-

Fill out this form to request additional mirroring.

-
- -
- - - - - diff --git a/scripts/templates/default/full.html b/scripts/templates/default/full.html deleted file mode 100644 index 3fd07e2..0000000 --- a/scripts/templates/default/full.html +++ /dev/null @@ -1,6 +0,0 @@ -
-

{{name}}

-

Last Updated: {{lastsync}}

-

Upstream: {{upstream}}

-
\ No newline at end of file diff --git a/scripts/templates/default/half.html b/scripts/templates/default/half.html deleted file mode 100644 index 0d807e6..0000000 --- a/scripts/templates/default/half.html +++ /dev/null @@ -1,6 +0,0 @@ -
-

{{name}}

-

Last Updated: {{lastsync}}

-

Upstream: {{upstream}}

-
\ No newline at end of file diff --git a/scripts/templates/new/base.html b/scripts/templates/new/base.html deleted file mode 100644 index 2dc4b48..0000000 --- a/scripts/templates/new/base.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - repo.city - - - - - - - - -
-

Welcome to REPO.CITY!

-

Simple mirroring & archiving server.

-
-
-
-

Mirroring List

-
- - {{ repos }} - -
- - -
-
-

Additional Mirror

- -
- - {{ repos_more }} - - -
-
-
-

Server Details

-

Location: Seoul, Korea

-

Bandwidth: ~1Gbps

-

Storage size: 32TiB

-

Sync delay: 4hrs

-

Contact: mirror@devpg.net

-

Maintainer: @morgan9e

-

This mirror is based on reposync.

-

For additional mirroring requests, please contact me at email above.

-
- -
- - - - - - \ No newline at end of file diff --git a/scripts/templates/new/full.html b/scripts/templates/new/full.html deleted file mode 100644 index dc513f2..0000000 --- a/scripts/templates/new/full.html +++ /dev/null @@ -1,6 +0,0 @@ -
-

{{name}}

-

Last Updated: {{lastsync}}

-

Upstream: {{upstream}}

-
\ No newline at end of file diff --git a/scripts/templates/new/half.html b/scripts/templates/new/half.html deleted file mode 100644 index 548ecf6..0000000 --- a/scripts/templates/new/half.html +++ /dev/null @@ -1,6 +0,0 @@ -
-

{{name}}

-

Last Updated: {{lastsync}}

-

Upstream: {{upstream}}

-
\ No newline at end of file diff --git a/scripts/yq b/scripts/yq deleted file mode 100755 index 0e63a90..0000000 Binary files a/scripts/yq and /dev/null differ diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100644 index f692302..0000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/form.html b/static/form.html deleted file mode 100644 index edde506..0000000 --- a/static/form.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - PAWE.ME - - - - - -
-

PAWE.ME

-

-
-
-

Request Form

-
- - -
- -
- - -
-
- -
-
-

  Only Rsync & HTTP File server is available. HTTP mirroring cycle is much slower than Rsync. Feel free to request!

- - - - diff --git a/static/index.html b/static/index.html deleted file mode 120000 index 79c5d6f..0000000 --- a/static/index.html +++ /dev/null @@ -1 +0,0 @@ -../index.html \ No newline at end of file diff --git a/static/log.html b/static/log.html deleted file mode 100644 index a70cf4e..0000000 --- a/static/log.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - Log Viewer - - - - - - - - - -
- - - - - - -