├── requirements.txt ├── screenshots ├── web.jpeg └── terminal.png ├── LICENSE ├── README.md ├── .gitignore └── ushare.py /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | typer 4 | pyqrcode 5 | python-multipart -------------------------------------------------------------------------------- /screenshots/web.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uniqueinx/ushare/HEAD/screenshots/web.jpeg -------------------------------------------------------------------------------- /screenshots/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uniqueinx/ushare/HEAD/screenshots/terminal.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ghazy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uShare 2 | 3 | A simple, elegant file sharing tool for local networks. Easily share files between devices with a web interface and QR code support. 4 | 5 | ## Features 6 | 7 | - **Simple File Sharing**: Share files quickly on your local network 8 | - **Web Interface**: Works on all devices 9 | - **QR Code Generation**: Easily connect from mobile devices by scanning a QR code 10 | - **Multiple File Upload**: Upload multiple files at once 11 | - **Cross-platform**: Works on any system with Python 12 | - **Zero Configuration**: Just run and share 13 | 14 | ## Installation 15 | 16 | 1. Clone the repository: 17 | ``` 18 | git clone https://github.com/uniqueinx/ushare.git 19 | cd ushare 20 | ``` 21 | 22 | 2. Install dependencies: 23 | ``` 24 | pip install -r requirements.txt 25 | ``` 26 | 27 | 3. Make the script executable (optional): 28 | ``` 29 | chmod +x ushare.py 30 | ``` 31 | 32 | ## Usage 33 | 34 | ``` 35 | Usage: ushare.py [OPTIONS] COMMAND [ARGS]... 36 | 37 | A simple tool to share files via browser in a local network 38 | 39 | Options: 40 | --help Show this message and exit. 41 | 42 | Commands: 43 | receive Receive files from others on the network. 44 | send Share a file with others on the network. 45 | ``` 46 | Examples: 47 | ``` 48 | ushare.py send file.txt 49 | ``` 50 | ``` 51 | usharer.py receive -d ./Downloads 52 | ``` 53 | 54 | 55 | ## Screenshots 56 | 57 | ### Web Interface 58 |  59 | 60 | ### Terminal Usage 61 |  -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /ushare.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import logging 3 | import os 4 | import socket 5 | import sys 6 | import re 7 | import unicodedata 8 | 9 | try: 10 | import typer 11 | import pyqrcode 12 | import uvicorn 13 | from fastapi import FastAPI, File, UploadFile 14 | from fastapi.responses import FileResponse, HTMLResponse 15 | from typing import List, Optional 16 | except Exception: 17 | print( 18 | "Missing dependencies, Try pip install pyqrcode fastapi uvicorn python-multipart typer" 19 | ) 20 | sys.exit(1) 21 | 22 | uvicorn_logger = logging.getLogger("uvicorn") 23 | fastapi_logger = logging.getLogger("fastapi") 24 | 25 | 26 | def secure_filename(filename): 27 | """ 28 | Return a secure version of a filename that is safe to store on the filesystem. 29 | """ 30 | # Normalize unicode characters 31 | filename = unicodedata.normalize("NFKD", filename) 32 | filename = filename.encode("ascii", "ignore").decode("ascii") 33 | 34 | # Remove illegal filesystem characters and sanitize the filename 35 | filename = re.sub(r"[^\w\s.-]", "", filename).strip() 36 | 37 | # Replace whitespace with underscores 38 | filename = re.sub(r"[\s]+", "_", filename) 39 | 40 | # If name was reduced to nothing, provide a default 41 | if not filename: 42 | filename = "unnamed_file" 43 | 44 | return filename 45 | 46 | 47 | app = FastAPI() 48 | app.config = {} 49 | app.config["UPLOAD_FOLDER"] = os.getcwd() 50 | 51 | gport = 9050 52 | gfile = "" 53 | gdirectory = os.getcwd() 54 | 55 | 56 | def get_local_ip(): 57 | """Get local IP address that other devices on the network can connect to.""" 58 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 59 | try: 60 | # Doesn't need to be reachable, just used to determine interface 61 | s.connect(("10.255.255.255", 1)) 62 | ip = s.getsockname()[0] 63 | except Exception: 64 | ip = "127.0.0.1" 65 | finally: 66 | s.close() 67 | return ip 68 | 69 | 70 | ip_address = get_local_ip() 71 | 72 | 73 | cli = typer.Typer( 74 | help="A simple tool to share files via browser in a local network", 75 | add_completion=False, 76 | ) 77 | 78 | 79 | @app.get("/", response_class=HTMLResponse) 80 | def index(): 81 | return index_template 82 | 83 | 84 | @app.get("/send") 85 | def send_(): 86 | try: 87 | file_path = os.path.join(gdirectory, gfile) 88 | if not os.path.exists(file_path): 89 | typer.echo("File not found, may be wrong path or filename.") 90 | sys.exit(1) 91 | return FileResponse(path=file_path, filename=gfile) 92 | except Exception: 93 | typer.echo("Error sending file.") 94 | sys.exit(1) 95 | 96 | 97 | @app.get("/receive", response_class=HTMLResponse) 98 | def receive_get(): 99 | return upload_template 100 | 101 | 102 | @app.post("/receive", response_class=HTMLResponse) 103 | async def receive_post(files: List[UploadFile] = File(...)): 104 | if not files or len(files) == 0: 105 | return failed_send_template 106 | 107 | for file in files: 108 | if file.filename == "": 109 | return failed_send_template 110 | filename = secure_filename(file.filename) 111 | file_content = await file.read() 112 | with open(os.path.join(app.config["UPLOAD_FOLDER"], filename), "wb") as f: 113 | f.write(file_content) 114 | 115 | return success_template 116 | 117 | 118 | def start_server(command: str, port: int, debug: bool = False): 119 | """Start the server with optional debug logging""" 120 | url = f"http://{ip_address}:{port}" 121 | qr = pyqrcode.create(url) 122 | typer.echo(qr.terminal(quiet_zone=1)) 123 | typer.echo(f"Link: {url}") 124 | typer.echo(f"Mode: {command.capitalize()}") 125 | typer.echo(f"Local network IP: {ip_address}") 126 | 127 | if debug: 128 | uvicorn_logger.setLevel(logging.INFO) 129 | fastapi_logger.setLevel(logging.INFO) 130 | typer.echo("Debug mode: Enabled") 131 | else: 132 | uvicorn_logger.setLevel(logging.ERROR) 133 | fastapi_logger.setLevel(logging.ERROR) 134 | 135 | typer.echo("Ctrl+C to exit") 136 | log_level = "info" if debug else "error" 137 | uvicorn.run(app, host="0.0.0.0", port=port, log_level=log_level) 138 | 139 | 140 | @cli.command() 141 | def send( 142 | file: str = typer.Argument(..., help="File to share", exists=True), 143 | port: int = typer.Option(gport, "--port", "-p", help="Port to use"), 144 | debug: bool = typer.Option(False, "--debug", help="Enable debug logging"), 145 | ): 146 | """ 147 | Share a file with others on the network. 148 | 149 | Example: 150 | ushare send myfile.pdf 151 | """ 152 | typer.echo(f"Sharing file: {file}") 153 | global gfile 154 | global gdirectory 155 | if file.count("/") > 1: 156 | gdirectory, gfile = file.rsplit("/", 1) 157 | else: 158 | gfile = file 159 | 160 | start_server("send", port, debug) 161 | 162 | 163 | @cli.command() 164 | def receive( 165 | directory: Optional[str] = typer.Option( 166 | None, "--directory", "-d", help="Folder where downloaded files will be saved" 167 | ), 168 | port: int = typer.Option(gport, "--port", "-p", help="Port to use"), 169 | debug: bool = typer.Option(False, "--debug", help="Enable debug logging"), 170 | ): 171 | """ 172 | Receive files from others on the network. 173 | 174 | Example: 175 | ushare receive 176 | ushare receive --directory ~/Downloads 177 | """ 178 | global upload_folder 179 | if directory: 180 | if not os.path.exists(directory): 181 | typer.echo(f"Directory {directory} does not exist") 182 | raise typer.Exit(1) 183 | typer.echo(f"Files will be saved to: {directory}") 184 | app.config["UPLOAD_FOLDER"] = directory 185 | 186 | start_server("receive", port, debug) 187 | 188 | 189 | index_template = """ 190 | 191 | 192 |
193 | 194 | 195 |297 | A simple tool to share files between devices on your local network. 298 | Choose an option below to get started. 299 |
300 | 301 | 309 | 310 | 313 |Your files have been successfully shared.
549 | Back to Home 550 |