Skip to content

Commit 48c3594

Browse files
committedMay 7, 2022
initial commit
1 parent e437cb3 commit 48c3594

File tree

7 files changed

+234
-0
lines changed

7 files changed

+234
-0
lines changed
 

‎config.ini

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[general]
2+
logdir = /home/mirror/mirror/logs.d
3+
chdir = /home/mirror/mirror
4+
timeformat = %%Y-%%m-%%d %%H:%%M (UTC+8)
5+
vars[tuna] = mirrors.tuna.tsinghua.edu.cn
6+
vars[ustc] = rsync.mirrors.ustc.edu.cn
7+
vars[neusoft] = mirrors.neusoft.edu.cn
8+
vars[rsync_args] = -6azH --delete --delete-after --delete-excluded --progress --info=progress2 --timeout=300
9+
vars[excludes] = --exclude-from=excludes.d/

‎excludes.d/.gitkeep

Whitespace-only changes.

‎logs.d/.gitkeep

Whitespace-only changes.

‎mirrorz.meta.json

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"version": 1.5,
3+
"site": {
4+
"url": "https://linux.xidian.edu.cn/mirrors",
5+
"logo": "https://linux.xidian.edu.cn/img/xdosc.svg",
6+
"logo_darkmode": "https://linux.xidian.edu.cn/img/xdosc.svg",
7+
"abbr": "Xidian",
8+
"name": "西电开源软件镜像站",
9+
"homepage": "https://linux.xidian.edu.cn",
10+
"issue": "https://linux.xidian.edu.cn/git/xdlinux/issues",
11+
"group": "Telegram: https://t.me/xdosc",
12+
"note": "仅面向校内使用"
13+
},
14+
"mirrors": [],
15+
"info": [],
16+
"extension": "D",
17+
"endpoints": [
18+
{
19+
"label": "xidian",
20+
"public": false,
21+
"resolve": "linux.xidian.edu.cn/mirrors",
22+
"filter": [
23+
"V4",
24+
"SSL",
25+
"NOSSL"
26+
],
27+
"range": [
28+
"222.25.128.0/18",
29+
"219.245.64.0/18",
30+
"219.244.112.0/20",
31+
"210.27.0.0/20",
32+
"202.117.112.0/20",
33+
"115.155.32.0/19",
34+
"115.155.0.0/19",
35+
"113.200.174.0/24",
36+
"113.140.11.0/24"
37+
]
38+
}
39+
]
40+
}

‎status.d/.gitkeep

Whitespace-only changes.

‎sync.py

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!env python3
2+
from pathlib import Path
3+
from datetime import datetime
4+
import subprocess
5+
import json
6+
import os
7+
import fcntl
8+
9+
from utils import get_logger, parse_config, update_status_json, parse_state
10+
11+
general, mirrors = parse_config()
12+
os.chdir(general['chdir'])
13+
log = get_logger(general['logdir'], 'sync')
14+
15+
16+
def do_sync(mirror, lastsuccess=None):
17+
status['state'] = f'Y{int(datetime.now().timestamp())}'
18+
if lastsuccess:
19+
status['state'] += f'O{int(lastsuccess.timestamp())}'
20+
log.info(f'syncing {mirror}')
21+
with open(path, 'w') as f:
22+
f.write(json.dumps(status))
23+
update_status_json(mirrors)
24+
25+
proc_output_path = Path('logs.d', 'output', f'{mirror}-{status["state"]}')
26+
proc_output = open(proc_output_path, 'w')
27+
proc = subprocess.Popen(
28+
executable='/bin/sh',
29+
args=["sh", "-c", status['command']],
30+
stderr=subprocess.STDOUT,
31+
stdout=proc_output,
32+
)
33+
proc_output.close()
34+
35+
try:
36+
code = proc.wait()
37+
except KeyboardInterrupt:
38+
code = -1
39+
40+
if code == 0:
41+
log.info(f'successfully synced {mirror}')
42+
message = f'Successfully Synced {mirror}.'
43+
proc_output_path.unlink()
44+
status['state'] = f'S{int(datetime.now().timestamp())}'
45+
elif code == -1:
46+
message = f'Paused Syncing {mirror}.'
47+
status['state'] = f'P{int(datetime.now().timestamp())}'
48+
if lastsuccess:
49+
message += f' Last Successful Sync: {lastsuccess.strftime(general["timeformat"])}.'
50+
status['state'] += f'O{int(lastsuccess.timestamp())}'
51+
else:
52+
message = f'Error Occured Syncing {mirror}.'
53+
log.error(f'error syncing {mirror}')
54+
status['state'] = f'F{int(datetime.now().timestamp())}'
55+
if lastsuccess:
56+
message += f' Last Successful Sync: {lastsuccess.strftime(general["timeformat"])}.'
57+
status['state'] += f'O{int(lastsuccess.timestamp())}'
58+
59+
with open(path, 'w') as f:
60+
f.write(json.dumps(status))
61+
update_status_json(mirrors)
62+
63+
64+
if os.path.exists('sync.lock'):
65+
print('sync.py already running...')
66+
exit()
67+
68+
with open('sync.lock', 'w+') as f:
69+
try:
70+
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
71+
except IOError:
72+
print('sync.py already running...')
73+
exit()
74+
75+
for mirror in mirrors.sections():
76+
path = Path('status.d', mirror)
77+
path.touch()
78+
with open(path, 'r') as f:
79+
status = f.read()
80+
try:
81+
status = json.loads(status)
82+
except json.JSONDecodeError:
83+
status = {}
84+
if not status:
85+
log.warning(f'{mirror} never synced, syncing for the first time...')
86+
status['name'] = mirror
87+
status['command'] = mirrors[mirror]['command'].format(**general['vars'])
88+
lastsuccess = None
89+
for state, time in parse_state(status.get('state') or ''): # follows mirrorz rules
90+
if state == 'S':
91+
if (datetime.now() - time).total_seconds() < 28800:
92+
log.info(f'skipping {mirror}, less than 8 hours since last sync')
93+
break
94+
lastsuccess = time
95+
elif state == 'Y':
96+
break
97+
elif state == 'O' and lastsuccess is None:
98+
lastsuccess = time
99+
else:
100+
do_sync(mirror, lastsuccess)
101+
102+
# execute this unconditionally on exit
103+
# to update status.json when mirrorz.meta.json changes
104+
update_status_json(mirrors)
105+
try:
106+
fcntl.flock(f, fcntl.LOCK_UN)
107+
finally:
108+
os.remove('sync.lock')

‎utils.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from configparser import ConfigParser
2+
from logging import Logger, handlers, Formatter
3+
from pathlib import Path
4+
from datetime import datetime
5+
import re
6+
import json
7+
from typing import Tuple
8+
9+
10+
def parse_config(file='config.ini'):
11+
config = ConfigParser()
12+
config.read(file)
13+
14+
general = {}
15+
for k, v in config['general'].items():
16+
if res := re.match(r'^(.*)\[(.*)\]$', k):
17+
if res[1] not in general:
18+
general[res[1]] = {}
19+
general[res[1]][res[2]] = v
20+
else:
21+
general[k] = v
22+
config.remove_section('general')
23+
return general, config
24+
25+
26+
def get_logger(logdir, name):
27+
log = Logger(name)
28+
handler = handlers.RotatingFileHandler(Path(logdir, f'{name}.log'), maxBytes=1024 * 128)
29+
handler.setFormatter(Formatter('[%(asctime)s] - %(message)s'))
30+
log.addHandler(handler)
31+
return log
32+
33+
34+
def timespan_fromtimestamp(ts):
35+
return (datetime.now() - datetime.fromtimestamp(ts))
36+
37+
38+
def update_status_json(mirrors):
39+
with open('mirrorz.meta.json', 'r') as f:
40+
status = json.loads(f.read())
41+
for mirror in mirrors.sections():
42+
path = Path('status.d', mirror)
43+
path.touch()
44+
obj = {
45+
"cname": mirror,
46+
"desc": mirrors[mirror].get('desc') or '',
47+
"url": mirrors[mirror].get('url') or f'/{mirror}',
48+
}
49+
with open(path, 'r') as f:
50+
try:
51+
job = json.loads(f.read())
52+
obj['status'] = job['state']
53+
except json.JSONDecodeError:
54+
obj['status'] = f'N{int(datetime.now().timestamp())}'
55+
status['mirrors'].append(obj)
56+
57+
status['mirrors'].sort(key=lambda x: x['cname'])
58+
with open('/srv/http/status.json.root/mirrors/status.json', 'w') as f:
59+
f.write(json.dumps(status))
60+
61+
62+
def parse_state(state: str) -> Tuple[str, datetime]:
63+
def next_ts(s: str, i: int):
64+
for j in range(i, len(s)):
65+
if not s[j].isdigit():
66+
return s[i:j - 1]
67+
return s[i:]
68+
i = 0
69+
while i < len(state):
70+
if state[i] in 'SYFPXNO':
71+
ts = next_ts(state, i + 1)
72+
time = datetime.fromtimestamp(int(ts))
73+
yield state[i], time
74+
i += len(ts)
75+
else:
76+
yield state[i], datetime.fromtimestamp(0)
77+
i += 1

0 commit comments

Comments
 (0)
Please sign in to comment.