"""Module for sending data and hosting sending servers."""
from aiohttp import web
import asyncio
import humanize
import magic
from multiprocessing import Process
import os
import pkgutil
import platform
import pyqrcode
import requests
from requests_toolbelt import MultipartEncoder
import socket
import tempfile
from time import sleep, strftime
from zipfile import ZipFile
from .exception import CodeExistsError, CodeNotFoundError, IsNotReceiverError
from .utils import get_local_ip_address, get_service_info, get_zip_file, \
register_service
__all__ = ["send", "send_server", "send_server_proc"]
# Request handlers
async def _text_page(request):
"""Renders a text viewing page, GET handler for route '/'."""
text = pkgutil.get_data(__name__, "static/text.html").decode()
return web.Response(text=text, content_type="text/html")
async def _text_sender(request):
"""Returns the text being shared, GET handler for route '/text'."""
address = ""
peername = request.transport.get_extra_info("peername")
if peername is not None:
host, _ = peername
address = " (by " + str(host) + ")"
print("Content viewed" + address + "!")
return web.Response(text=request.app["text"])
async def _download_page(request):
"""Renders a download page, GET handler for route '/'."""
download = pkgutil.get_data(__name__, "static/download.html").decode()
return web.Response(text=download, content_type="text/html")
async def _file_stream_sender(request):
"""Streams a file from the server, GET handler for route '/download'."""
address = ""
peername = request.transport.get_extra_info("peername")
if peername is not None:
host, _ = peername
address = " (by " + str(host) + ")"
if request.method == "GET":
print("Content requested" + address + ", transferred!")
elif request.method == "HEAD":
print("Content examined" + address + "!")
response = web.StreamResponse()
file_path = request.app["file_path"]
file_name = request.app["file_name"]
file_size = str(request.app["file_size"])
header = "attachment; filename={}; size={}".format(file_name, file_size)
response.headers["content-type"] = magic.Magic(mime=True) \
.from_file(file_path)
response.headers["content-length"] = str(request.app["file_size"])
response.headers["content-disposition"] = header
await response.prepare(request)
with open(file_path, "rb") as f:
chunk = f.read(8192)
while chunk:
await response.write(chunk)
chunk = f.read(8192)
return response
async def _is_airshare_text_sender(request):
"""Returns 'Text Sender', GET handler for route '/airshare'."""
return web.Response(text="Text Sender")
async def _is_airshare_file_sender(request):
"""Returns 'File Sender', GET handler for route '/airshare'."""
return web.Response(text="File Sender")
# Sender functions
[docs]def send(*, code, file, compress=False):
r"""Send file(s) or directories to a receiving server.
Parameters
----------
code : str
Identifying code for the Airshare receiving server.
file : str or list or None
Relative path or list of paths of the files or directories to serve.
For multiple files or directories, contents are automatically zipped.
compress : boolean, default=False
Flag to enable or disable compression (Zip).
Effective when only one file is given.
Returns
-------
status_code : int
Status code of upload POST request.
"""
info = get_service_info(code)
if info is None:
raise CodeNotFoundError(code)
if type(file) is str:
if file == "":
file = None
else:
file = [file]
elif len(file) == 0:
file = None
if file is None:
raise ValueError("The parameter `file` must be non-empty!")
if compress or len(file) > 1 or os.path.isdir(file[0]):
print("Compressing...")
file, name = get_zip_file(file)
print("Compressed to `" + name + "`!")
else:
file, name = file[0], file[0].split(os.path.sep)[-1]
ip = socket.inet_ntoa(info.addresses[0])
url = "http://" + ip + ":" + str(info.port)
airshare_type = requests.get(url + "/airshare")
if airshare_type.text != "Upload Receiver":
raise IsNotReceiverError(code)
m = MultipartEncoder(fields={"field0": (name, open(file, "rb"))})
headers = {"content-type": m.content_type}
r = requests.post(url + "/upload", data=m, headers=headers)
print("Uploaded `" + name + "` to Airshare `" + code + ".local`!")
return r.status_code
[docs]def send_server(*, code, text=None, file=None, compress=False, port=80):
r"""Serves a file or text and registers it as a Multicast-DNS service.
Parameters
----------
code : str
Identifying code for the Airshare service and server.
text : str or None
String value to be shared.
If both `text` and `files` are given, `text` will be shared.
Must be given if `files` is not given.
file : str or list or None
Relative path or list of paths of the files or directories to serve. If
multiple files or directories are given, the contents are automatically
zipped. If not given or both `files` and `text` are given, `text` will
be shared. Must be given if `text` is not given.
compress : boolean, default=False
Flag to enable or disable compression (Zip).
Effective when only one file is given.
port : int, default=80
Port number at which the server is hosted on the device.
"""
info = get_service_info(code)
if info is not None:
raise CodeExistsError(code)
if file is not None:
if type(file) is str:
if file == "":
file = None
else:
file = [file]
elif len(file) == 0:
file = None
content = text or file
name = None
if content is None:
raise ValueError("Either `file` or `text` (keyword arguments) must be"
+ " given and non-empty!")
elif text is None and file is not None:
if compress or len(file) > 1 or os.path.isdir(file[0]):
print("Compressing...")
content, name = get_zip_file(file)
print("Compressed to `" + name + "`!")
else:
content = file[0]
addresses = [get_local_ip_address()]
register_service(code, addresses, port)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
app = web.Application()
file_size = ""
if text is not None:
app["text"] = content
app.router.add_get(path="/", handler=_text_page)
app.router.add_get(path="/text", handler=_text_sender)
app.router.add_get(path="/airshare", handler=_is_airshare_text_sender)
elif file:
app["file_path"] = os.path.realpath(content)
app["file_name"] = name or app["file_path"].split(os.path.sep)[-1]
app["file_size"] = os.stat(app["file_path"]).st_size
file_size = " (" + humanize.naturalsize(app["file_size"]) + ") "
content = app["file_name"]
app.router.add_get(path="/", handler=_download_page)
app.router.add_get(path="/airshare", handler=_is_airshare_file_sender)
app.router.add_get(path="/download", handler=_file_stream_sender)
runner = web.AppRunner(app)
loop.run_until_complete(runner.setup())
site = web.TCPSite(runner, "0.0.0.0", str(port))
loop.run_until_complete(site.start())
url_port = ""
if port != 80:
url_port = ":" + str(port)
ip = socket.inet_ntoa(addresses[0]) + url_port
print("`" + content + "`" + file_size + "available at " + ip
+ " and `http://" + code + ".local" + url_port + "`, press CtrlC"
+ " to stop sharing...")
if platform.system() != "Windows":
print(pyqrcode.create("http://" + ip + url_port)
.terminal(quiet_zone=1))
loop.run_forever()
[docs]def send_server_proc(*, code, text=None, file=None, compress=False, port=80):
r"""Creates a process with 'send_server' as the target.
Parameters
----------
code : str
Identifying code for the Airshare service and server.
text : str or None
String value to be shared.
If both `text` and `files` are given, `text` will be shared.
Must be given if `files` is not given.
file : str or list or None
Relative path or list of paths of the files or directories to serve. If
multiple files or directories are given, the contents are automatically
zipped. If not given or both `files` and `text` are given, `text` will
be shared. Must be given if `text` is not given.
compress : boolean, default=False
Flag to enable or disable compression (Zip).
Effective when only one file is given.
port : int, default=80
Port number at which the server is hosted on the device.
Returns
-------
process: multiprocessing.Process
A multiprocessing.Process object with 'send_server' as target.
"""
kwargs = {"code": code, "file": file, "text": text, "compress": compress,
"port": port}
process = Process(target=send_server, kwargs=kwargs)
return process