Skip to content

Commit de4ea71

Browse files
committed
Initial commit
0 parents  commit de4ea71

File tree

8 files changed

+901
-0
lines changed

8 files changed

+901
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__/
2+
build/

LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Flashpoint Updater
2+
3+
The updater for BlueMaxima's Flashpoint.
4+
Currently a work in progress.
5+
6+
## End-user Setup
7+
8+
### Windows
9+
10+
1. Download the latest release.
11+
2. Unpack anywhere.
12+
3. Run it from the command-line as such:
13+
`update.exe <flashpoint-path> <current-version> <target-version>`
14+
15+
##### Example: `update.exe C:\Flashpoint 5.5 6.1`
16+
17+
### Mac/Linux
18+
19+
1. Install Python 3.
20+
2. Clone the repository.
21+
3. Run in the project root: `pip install -r requirements.txt`
22+
4. Use it like: `update.py /media/ext1/Flashpoint <flashpoint-path> <current-version> <target-version>`
23+
24+
## Server Setup
25+
26+
The updater works by fetching differing files from two version indexes. These indexes contain SHA-1 hashes of all the files in the project mapped to an array of their paths.
27+
28+
The updater script expects indexes to be available at the location specified by `index_endpoint` in `config.json`. Example: `https://unstable.life/fp-index/6.1.json.xz`
29+
30+
Similarly, files will be fetched in the location specified by `file_endpoint`.
31+
32+
To generate indexes, use `index.py`: `index.py /media/ext1/Flashpoint 6.2.json.xz`

config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"index_endpoint": "https://unstable.life/fp-index",
3+
"file_endpoint": "https://unstable.life/fp"
4+
}

index.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env python3
2+
from tqdm import tqdm
3+
import json
4+
import lzma
5+
import os
6+
import sys
7+
import hashlib
8+
import posixpath
9+
10+
# Allows accessing files that exceed MAX_PATH in Windows
11+
# See: https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file#maximum-path-length-limitation
12+
def win_path(path):
13+
if os.name == 'nt':
14+
path = os.path.abspath(path)
15+
prefix = '\\\\?\\'
16+
if not path.startswith(prefix):
17+
# Handle shared paths
18+
if path.startswith('\\\\'):
19+
prefix += 'UNC'
20+
path = path[1::] # Remove leading slash
21+
path = prefix + path
22+
return path
23+
24+
def hash(file, hashalg, bufsize=2**16):
25+
hash = hashlib.new(hashalg)
26+
with open(file, 'rb') as f:
27+
buf = f.read(bufsize)
28+
while len(buf) > 0:
29+
hash.update(buf)
30+
buf = f.read(bufsize)
31+
return hash.hexdigest()
32+
33+
def index(path, hashalg):
34+
files = dict()
35+
empty = list()
36+
path = win_path(path)
37+
with tqdm(unit=' files') as pbar:
38+
for r, d, f in os.walk(path):
39+
# Include empty folders
40+
rel = os.path.relpath(r, path).replace(os.path.sep, '/')
41+
if len(d) == 0 and len(f) == 0:
42+
empty.append(rel)
43+
else:
44+
for x, f in ((x if rel == '.' else posixpath.join(rel, x), os.path.join(r, x)) for x in f):
45+
files.setdefault(hash(f, hashalg), list()).append(x)
46+
pbar.update(1)
47+
return files, empty
48+
49+
if __name__ == '__main__':
50+
51+
if len(sys.argv) != 3:
52+
print('Usage: index.py <path> <out.json.xz>')
53+
sys.exit(0)
54+
55+
files, empty = index(sys.argv[1], 'sha1')
56+
print('Applying LZMA compression...')
57+
with lzma.open(sys.argv[2], 'wt', encoding='utf-8', preset=9) as f:
58+
json.dump({'files': files, 'empty': empty}, f, separators=(',', ':'), ensure_ascii=False)

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
requests
2+
backoff
3+
tqdm

setup.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import sys
2+
import os
3+
from cx_Freeze import setup, Executable
4+
5+
# Dependencies are automatically detected, but it might need fine tuning.
6+
build_exe_options = {"packages": ["os", "asyncio", "idna.idnadata"], "excludes": ["numpy", "matplotlib"], 'include_files': ['config.json']}
7+
8+
PYTHON_INSTALL_DIR = os.path.dirname(os.path.dirname(os.__file__))
9+
if sys.platform == "win32":
10+
build_exe_options['include_files'] += [
11+
os.path.join(PYTHON_INSTALL_DIR, 'DLLs', 'libcrypto-1_1.dll'),
12+
os.path.join(PYTHON_INSTALL_DIR, 'DLLs', 'libssl-1_1.dll'),
13+
]
14+
15+
setup( name = "flashpoint-updater",
16+
version = "0.1",
17+
description = "Updater for BlueMaxima's Flashpoint",
18+
options = {"build_exe": build_exe_options},
19+
executables = [Executable("update.py"), Executable("index.py")])

update.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
from index import win_path
3+
from tqdm import tqdm
4+
from urllib.parse import quote
5+
from concurrent.futures import as_completed
6+
import concurrent.futures
7+
import urllib3
8+
import datetime
9+
import requests
10+
import backoff
11+
import shutil
12+
import stat
13+
import json
14+
import lzma
15+
import time
16+
import sys
17+
import os
18+
19+
@backoff.on_exception(backoff.expo, (requests.exceptions.RequestException, urllib3.exceptions.ProtocolError))
20+
def download_file(session, url, dest):
21+
with session.get(url, stream=True, timeout=10) as r:
22+
with open(dest, 'wb') as f:
23+
shutil.copyfileobj(r.raw, f)
24+
25+
# Fix for "read-only" files on Windows
26+
def chown_file(path):
27+
os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
28+
29+
def fetch_index(version, endpoint):
30+
r = requests.get('%s/%s.json.xz' % (endpoint, version))
31+
return json.loads(lzma.decompress(r.content))
32+
33+
if __name__ == '__main__':
34+
35+
with open('config.json', 'r') as f:
36+
config = json.load(f)
37+
38+
if len(sys.argv) != 4:
39+
print('Usage: update.py <flashpoint-path> <current-version> <target-version>')
40+
sys.exit(0)
41+
42+
flashpoint = win_path(sys.argv[1])
43+
if not os.path.isdir(flashpoint):
44+
print('Error: Flashpoint path not found.')
45+
sys.exit(0)
46+
47+
endpoint = config['index_endpoint']
48+
try:
49+
current, target = fetch_index(sys.argv[2], endpoint), fetch_index(sys.argv[3], endpoint)
50+
except requests.exceptions.RequestException:
51+
print('Could not retrieve indexes for the versions specified.')
52+
sys.exit(0)
53+
54+
start = time.time()
55+
tmp = os.path.join(flashpoint, '.tmp')
56+
os.mkdir(tmp)
57+
to_download = list()
58+
print('Preparing contents...')
59+
for hash in tqdm(target['files'], unit=' files', ascii=True):
60+
if hash in current['files']:
61+
path = os.path.normpath(current['files'][hash][0])
62+
os.rename(os.path.join(flashpoint, path), os.path.join(tmp, hash))
63+
else:
64+
to_download.append(hash)
65+
66+
print('Downloading new data...')
67+
session = requests.Session()
68+
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
69+
tasks = list()
70+
for hash in to_download:
71+
url = '%s/%s' % (config['file_endpoint'], quote(target['files'][hash][0]))
72+
tasks.append(executor.submit(download_file, session, url, os.path.join(tmp, hash)))
73+
for future in tqdm(as_completed(tasks), total=len(tasks), unit=' files', ascii=True):
74+
future.result()
75+
76+
print('Removing obsolete files...')
77+
for r, d, f in os.walk(flashpoint, topdown=False):
78+
if r == tmp:
79+
continue
80+
for x in f:
81+
path = os.path.join(r, x)
82+
chown_file(path)
83+
os.remove(path)
84+
for x in d:
85+
path = os.path.join(r, x)
86+
if path != tmp:
87+
chown_file(path)
88+
os.rmdir(path)
89+
90+
print('Creating file structure...')
91+
for hash in tqdm(target['files'], unit=' files', ascii=True):
92+
paths = target['files'][hash]
93+
while paths:
94+
path = os.path.normpath(paths.pop(0))
95+
parent = os.path.dirname(path)
96+
if parent:
97+
os.makedirs(os.path.join(flashpoint, parent), exist_ok=True)
98+
tmpfile = os.path.join(tmp, hash)
99+
dest = os.path.join(flashpoint, path)
100+
if paths:
101+
shutil.copy(tmpfile, dest)
102+
else: # No more paths, we can move instead
103+
os.rename(tmpfile, dest)
104+
105+
for path in target['empty']:
106+
os.makedirs(os.path.join(flashpoint, os.path.normpath(path)))
107+
108+
os.rmdir(tmp)
109+
print('Update completed in %s' % str(datetime.timedelta(seconds=time.time() - start)))

0 commit comments

Comments
 (0)