legendary/legendary/utils/savegame_helper.py
derrod 36c1a9591f [utils] Add helper for packing save data
This helper creates manifests + chunks from
a given directory for uploading to the EOS
save game storage service.
2020-05-12 04:03:03 +02:00

116 lines
4.4 KiB
Python

import logging
import os
import time
from hashlib import sha1
from io import BytesIO
from tempfile import TemporaryFile
from legendary.models.chunk import Chunk
from legendary.models.manifest import \
Manifest, ManifestMeta, CDL, FML, CustomFields, FileManifest, ChunkPart, ChunkInfo
class SaveGameHelper:
def __init__(self):
self.files = dict()
self.log = logging.getLogger('SGH')
def finalize_chunk(self, chunk: Chunk):
ci = ChunkInfo()
ci.guid = chunk.guid
ci.hash = chunk.hash
ci.sha_hash = chunk.sha_hash
# use a temporary file for uploading
_tmp_file = TemporaryFile()
self.files[ci.path] = _tmp_file
# write() returns file size and also sets the uncompressed size
ci.file_size = chunk.write(_tmp_file)
ci.window_size = chunk.uncompressed_size
_tmp_file.seek(0)
return ci
def package_savegame(self, input_folder: str, app_name: str = '',
epic_id: str = '', cloud_folder: str = ''):
"""
:param input_folder: Folder to be packaged into chunks/manifest
:param app_name: App name for savegame being stored
:param epic_id: Epic account ID
:param cloud_folder: Folder the savegame resides in (based on game metadata)
:return:
"""
m = Manifest()
m.meta = ManifestMeta()
m.chunk_data_list = CDL()
m.file_manifest_list = FML()
m.custom_fields = CustomFields()
# create metadata for savegame
m.meta.app_name = f'{app_name}{epic_id}'
m.meta.build_version = time.strftime('%Y.%m.%d-%H.%M.%S')
m.custom_fields['CloudSaveFolder'] = cloud_folder
self.log.info(f'Packing savegame for "{app_name}", input folder: {input_folder}')
files = []
for _dir, _, _files in os.walk(input_folder):
for _file in _files:
files.append(os.path.join(_dir, _file))
chunk_num = 0
cur_chunk = None
cur_buffer = None
for _file in sorted(files, key=str.casefold):
s = os.stat(_file)
f = FileManifest()
# get relative path for manifest
f.filename = os.path.relpath(_file, input_folder).replace('\\', '/')
self.log.debug(f'Processing file "{f.filename}"')
f.file_size = s.st_size
fhash = sha1()
with open(_file, 'rb') as cf:
while remaining := s.st_size - cf.tell():
if not cur_chunk: # create new chunk
cur_chunk = Chunk()
if cur_buffer:
cur_buffer.close()
cur_buffer = BytesIO()
chunk_num += 1
# create chunk part and write it to chunk buffer
cp = ChunkPart(guid=cur_chunk.guid, offset=cur_buffer.tell(),
size=min(remaining, 1024 * 1024 - cur_buffer.tell()))
_tmp = cf.read(cp.size)
cur_buffer.write(_tmp)
fhash.update(_tmp) # update sha1 hash with new data
f.chunk_parts.append(cp)
if cur_buffer.tell() >= 1024 * 1024:
cur_chunk.data = cur_buffer.getvalue()
ci = self.finalize_chunk(cur_chunk)
self.log.info(f'Chunk #{chunk_num} "{ci.path}" created')
# add chunk to CDL
m.chunk_data_list.elements.append(ci)
cur_chunk = None
f.hash = fhash.digest()
m.file_manifest_list.elements.append(f)
# write remaining chunk if it exists
if cur_chunk:
cur_chunk.data = cur_buffer.getvalue()
ci = self.finalize_chunk(cur_chunk)
self.log.info(f'Chunk #{chunk_num} "{ci.path}" created')
m.chunk_data_list.elements.append(ci)
cur_buffer.close()
# Finally write/serialize manifest into another temporary file
_m_filename = f'manifests/{m.meta.build_version}.manifest'
_tmp_file = TemporaryFile()
_m_size = m.write(_tmp_file)
_tmp_file.seek(0)
self.log.info(f'Manifest "{_m_filename}" written ({_m_size} bytes)')
self.files[_m_filename] = _tmp_file
# return dict with created files for uploading/whatever
return self.files