diff --git a/main.py b/main.py index a826bff..219165c 100644 --- a/main.py +++ b/main.py @@ -16,8 +16,24 @@ import aiofiles import qrcode -qrcodes: dict[str, str] = {} -progress: dict[str, float] = {} +class RomFile: + """ A RomFile holds all state information for real ROM files found in the cwd """ + file: str + qrcode: str + progress: float + + def __init__(self, file: str): + self.file = file + self.progress = 0 + +class RomFileEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, RomFile): + return obj.__dict__ + return super().default(obj) + +ip_addr: str +romfiles: dict[str, RomFile] = dict() INDEX_PAGE = """ @@ -60,6 +76,7 @@ color: #000; border-radius: 4rem; padding: 4rem; + padding-bottom: 2rem; } .qr img { opacity: 0.125; @@ -77,7 +94,7 @@ -

Your CIAServer is running!

+

Your CIAServer is running!

Keep the terminal window open to keep the server online.

@@ -86,28 +103,74 @@ codes and install software via FBI (In Scan QR Code mode). Hover over a QR code to make it easier to scan.

-
- {{content}} -
+
""" async def file_sender(file_path): """ Streams files without loading into memory """ + global romfiles + chunk_size = 2 ** 16 file_size = os.path.getsize(file_path) total_sent = 0 @@ -119,47 +182,42 @@ async def file_sender(file_path): break yield chunk total_sent += len(chunk) - progress[file_path] = total_sent / file_size + romfiles[file_path].progress = total_sent / file_size async def get_cia(request:web.Request): """ Respond to GET requests with the rom file """ file = request.path[1:] + print(f"Serving {file}...") if not os.path.exists(file): return web.Response(body=f"File ({file}) does not exist", status=404) headers = { - "Content-disposition": f"attachment; filename={file}", + "Content-disposition": f"attachment; filename={file.replace(',','_')}", "content-type":"application/octet-stream", - "accept-ranges":"bytes" + "accept-ranges":"bytes", + "content-length": str(os.path.getsize(file)) } response = web.StreamResponse(headers=headers) - response.enable_chunked_encoding() - response.headers.add('content-length', str(os.path.getsize(file))) - async def stream_file(): async for chunk in file_sender(file): await response.write(chunk) - await response.prepare(request) - await stream_file() + try: + await response.prepare(request) + await stream_file() + except ConnectionResetError: + print("Connection aborted: other end cancelled.") + return return response async def get_home(_:web.Request): """ Generates an html document full of all generated QR codes """ headers = {'content-type': 'text/html; charset=utf-8'} - content = '\n'.join( - (f""" -
- QR code that downloads {file} -

{file}

- -
""" for file,data in qrcodes.items()) - ) return web.Response( - body=INDEX_PAGE.replace('{{content}}', content), + body=INDEX_PAGE, headers=headers ) @@ -168,33 +226,39 @@ async def get_progress(_:web.Request): headers = {'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache', 'x-content-type-options': 'nosniff'} - content = json.dumps(progress) + content = json.dumps(list(romfiles.values()), cls=RomFileEncoder) return web.Response(body=content, headers=headers) +async def file_crawler(): + """ Maintains a list of rom files that currently exist """ + global romfiles + + while True: + foundroms = [] + for typ in ['*.cia','*.3dsx']: + for foundfile in glob.glob(typ): + foundroms.append(foundfile) + if foundfile and (foundfile not in romfiles): + romfiles[foundfile] = RomFile(foundfile) + print("Found "+foundfile) + romfiles[foundfile].qrcode = generate_qr(foundfile, ip_addr) + + for rom in romfiles: + if rom not in foundroms: + del romfiles[rom] + break # can't continue iterating once romfiles is changed + + await asyncio.sleep(1) + async def main(): - """ Main loop """ - global qrcodes - - app=web.Application() - # Find rom files and create routes - #TODO: add support for noticing changes to the working directory - roms=[] - for typ in ['*.cia','*.3dsx']: - result=glob.glob(typ) - if result: - roms+=result - app.add_routes([web.get('/'+parse.quote_plus(f).replace('+','%20'),get_cia) for f in roms]) + """ Main process entrypoint """ + global ip_addr + + app = web.Application() app.router.add_get('/', get_home) app.router.add_get('/progress', get_progress) + app.router.add_get('/{file_path:.+}',get_cia) - if len(roms)<=0: - print('error: please place .cia files in the same directory as this script before running!') - return - print('Found '+str(len(roms))+' file(s) to share with 3ds clients.') - print('Open your homebrew software manager of choice (FBI) on your device', - 'and find the scan qr code option now.\n') - - ip_addr='0.0.0.0' if os.path.exists('ip override.txt'): with open('ip override.txt','r',encoding='ascii') as f: ip_addr=f.read() @@ -206,6 +270,9 @@ async def main(): s.connect(("8.8.8.8", 80)) ip_addr = s.getsockname()[0] + # Start the file crawler once the ip address is known + asyncio.ensure_future(file_crawler()) + print('Starting web server at '+ip_addr+':8888...') runner = web.AppRunner(app) @@ -219,8 +286,6 @@ async def main(): return print(f'Web server running at http://{ip_addr}:8888 !\n') - qrcodes = dict((f, generate_qr(f, ip_addr)) for f in roms) - print("\nDone! Scan each of these qr codes with each cracked 3ds you want them installed on!") print('Keep this window open in order to keep the transmission running.') print('You can transfer multiple apps to multiple cracked 3dses at once.')