├── .github └── FUNDING.yml ├── logo.png ├── requirements.txt ├── LICENSE ├── README.md └── fileshare.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: dopevog 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopevog/fileshare/HEAD/logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | netifaces 2 | qrcode 3 | colorama; platform_system == "Windows" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vedant Kothari 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Fileshare

2 |


3 |

4 |

📨 Share files easily over your local network from the terminal! 📨

5 | 6 | 7 | ## Installation 8 | 9 | ```bash 10 | # clone the repo 11 | $ git clone https://github.com/dopevog/fileshare.git 12 | 13 | # change the working directory to fileshare 14 | $ cd fileshare 15 | 16 | # install the requirements 17 | $ pip3 install -r requirements.txt 18 | ``` 19 | 20 | 21 | ## Usage 22 | ``` 23 | usage: python fileshare [-h] [--debug] [--receive] [--port PORT] 24 | [--ip_addr {192.168.0.105}] [--auth AUTH] 25 | file_path 26 | 27 | Transfer files over WiFi between your computer and your smartphone from the 28 | terminal 29 | 30 | positional arguments: 31 | file_path path that you want to transfer or store the received 32 | file. 33 | 34 | optional arguments: 35 | -h, --help show this help message and exit 36 | --debug, -d show the encoded url. 37 | --receive, -r enable upload mode, received file will be stored at 38 | given path. 39 | --port PORT, -p PORT use a custom port 40 | --ip_addr {192.168.0.105} specify IP address 41 | --auth AUTH add authentication, format: username:password 42 | --no-force-download Allow browser to handle the file processing instead of 43 | forcing it to download. 44 | ``` 45 | 46 | **Note:** Both devices needs to be connected to the same network 47 | 48 | **Exiting:** To exit the program, just press ```CTRL+C```. 49 | 50 | --- 51 | 52 | Transfer a single file 53 | ```bash 54 | $ python fileshare.py /path/to/file.png 55 | ``` 56 | 57 | 58 | Transfer a full directory. **Note:** the directory gets zipped before being transferred 59 | ```bash 60 | $ python fileshare.py /path/to/directory/ 61 | ``` 62 | 63 | Receive/upload a file from your phone to your computer 64 | ```bash 65 | $ python fileshare.py -r /path/to/receive/file/to/ 66 | ``` 67 | 68 | ## License 69 | This Project Has Been [MIT Licensed](https://github.com/cgraphite/fileshare/blob/main/LICENSE) 70 | -------------------------------------------------------------------------------- /fileshare.py: -------------------------------------------------------------------------------- 1 | import http.server 2 | import html 3 | import socketserver 4 | import random 5 | import os 6 | import socket 7 | import sys 8 | import shutil 9 | from shutil import make_archive 10 | import pathlib 11 | import signal 12 | import platform 13 | import argparse 14 | import urllib.request 15 | import urllib.parse 16 | import urllib.error 17 | import re 18 | from io import BytesIO 19 | import qrcode 20 | import base64 21 | 22 | 23 | MacOS = "Darwin" 24 | Linux = "Linux" 25 | Windows = "Windows" 26 | operating_system = platform.system() 27 | message = """ 28 | This port is being used. Try another port. 29 | """ 30 | 31 | 32 | def cursor(status): 33 | if operating_system != Windows: 34 | print("\033[?25" + ("h" if status else "l"), end="") 35 | 36 | 37 | def clean_exit(): 38 | cursor(True) 39 | 40 | print("\r", end="") 41 | 42 | print(" ") 43 | 44 | sys.exit() 45 | 46 | 47 | def FileTransferServerHandlerClass(file_name, auth, debug, no_force_download): 48 | class FileTransferServerHandler(http.server.SimpleHTTPRequestHandler): 49 | _file_name = file_name 50 | _auth = auth 51 | _debug = debug 52 | _no_force_download = no_force_download 53 | 54 | def do_AUTHHEAD(self): 55 | self.send_response(401) 56 | self.send_header("WWW-Authenticate", 'Basic realm="fileshare"') 57 | self.send_header("Content-type", "text/html") 58 | self.end_headers() 59 | 60 | def do_GET(self): 61 | if self._auth is not None: 62 | if self.headers.get("Authorization") != "Basic " + ( 63 | self._auth.decode() 64 | ): 65 | self.do_AUTHHEAD() 66 | return 67 | 68 | request_path = self.path[1:] 69 | if request_path != self._file_name: 70 | self.send_response(403) 71 | self.send_header("Content-type", "text/html") 72 | self.end_headers() 73 | else: 74 | super().do_GET() 75 | 76 | def guess_type(self, path): 77 | if not self._no_force_download: 78 | return "application/octet-stream" 79 | 80 | super().guess_type(path) 81 | 82 | def log_message(self, format, *args): 83 | if self._debug: 84 | super().log_message(format, *args) 85 | 86 | return FileTransferServerHandler 87 | 88 | 89 | def FileUploadServerHandlerClass(output_dir, auth, debug): 90 | class FileUploadServerHandler(http.server.BaseHTTPRequestHandler): 91 | absolute_path = os.path.abspath(output_dir) 92 | home = os.path.expanduser("~") 93 | nice_path = absolute_path.replace(home, "~") 94 | _output_dir = output_dir 95 | _auth = auth 96 | _debug = debug 97 | 98 | def do_AUTHHEAD(self): 99 | self.send_response(401) 100 | self.send_header("WWW-Authenticate", 'Basic realm="fileshare"') 101 | self.send_header("Content-type", "text/html") 102 | self.end_headers() 103 | 104 | def do_GET(self): 105 | if self._auth is not None: 106 | if self.headers.get("Authorization") != "Basic " + ( 107 | self._auth.decode() 108 | ): 109 | self.do_AUTHHEAD() 110 | return 111 | 112 | f = self.send_head() 113 | if f: 114 | self.copyfile(f, self.wfile) 115 | f.close() 116 | 117 | def do_HEAD(self): 118 | f = self.send_head() 119 | if f: 120 | f.close() 121 | 122 | def do_POST(self): 123 | """Serve a POST request.""" 124 | r, info = self.deal_post_data() 125 | print((r, info, "by: ", self.client_address)) 126 | 127 | f = BytesIO() 128 | f.write(b'') 129 | f.write(b"fileshare") 130 | f.write( 131 | b'' 132 | ) 133 | f.write( 134 | b'' 135 | ) 136 | f.write(b'') 137 | f.write(b"
") 138 | f.write(b"") 139 | f.write( 140 | b"

Upload Result Page

" 141 | ) 142 | f.write(b"
") 143 | 144 | if r: 145 | f.write( 146 | b"Success: " 147 | ) 148 | else: 149 | f.write( 150 | b"Failed: " 151 | ) 152 | 153 | f.write( 154 | ( 155 | "%s
" 156 | % info 157 | ).encode() 158 | ) 159 | f.write( 160 | ( 161 | "
back" 162 | % self.headers["referer"] 163 | ).encode() 164 | ) 165 | f.write( 166 | b"
Made By: " 167 | ) 168 | f.write(b'') 169 | f.write(b"Dopevog\n") 170 | length = f.tell() 171 | f.seek(0) 172 | self.send_response(200) 173 | self.send_header("Content-type", "text/html; charset=utf-8") 174 | self.send_header("Content-Length", str(length)) 175 | self.end_headers() 176 | if f: 177 | self.copyfile(f, self.wfile) 178 | f.close() 179 | 180 | def log_message(self, format, *args): 181 | if self._debug: 182 | super().log_message(format, *args) 183 | 184 | def deal_post_data(self): 185 | uploaded_files = [] 186 | content_type = self.headers["content-type"] 187 | if not content_type: 188 | return (False, "Content-Type header doesn't contain boundary") 189 | boundary = content_type.split("=")[1].encode() 190 | remainbytes = int(self.headers["content-length"]) 191 | line = self.rfile.readline() 192 | remainbytes -= len(line) 193 | 194 | if boundary not in line: 195 | return (False, "Content NOT begin with boundary") 196 | while remainbytes > 0: 197 | line = self.rfile.readline() 198 | remainbytes -= len(line) 199 | fn = re.findall( 200 | r'Content-Disposition.*name="file"; filename="(.*)"', 201 | line.decode("utf-8", "backslashreplace"), 202 | ) 203 | if not fn: 204 | return (False, "Can't find out file name...") 205 | file_name = fn[0] 206 | fn = os.path.join(self._output_dir, file_name) 207 | line = self.rfile.readline() 208 | remainbytes -= len(line) 209 | line = self.rfile.readline() 210 | remainbytes -= len(line) 211 | try: 212 | out = open(fn, "wb") 213 | except IOError: 214 | return ( 215 | False, 216 | "Can't create file to write, do you have permission to write?", 217 | ) 218 | else: 219 | with out: 220 | preline = self.rfile.readline() 221 | remainbytes -= len(preline) 222 | while remainbytes > 0: 223 | line = self.rfile.readline() 224 | remainbytes -= len(line) 225 | if boundary in line: 226 | preline = preline[0:-1] 227 | if preline.endswith(b"\r"): 228 | preline = preline[0:-1] 229 | out.write(preline) 230 | uploaded_files.append( 231 | os.path.join(self.nice_path, file_name) 232 | ) 233 | break 234 | else: 235 | out.write(preline) 236 | preline = line 237 | return (True, "File '%s' upload success!" % ",".join(uploaded_files)) 238 | 239 | def send_head(self): 240 | f = BytesIO() 241 | displaypath = html.escape(urllib.parse.unquote(self.nice_path)) 242 | 243 | f.write(b"fileshare") 244 | f.write( 245 | b'' 246 | ) 247 | f.write( 248 | b'' 249 | ) 250 | f.write(b'') 251 | f.write(b"") 252 | f.write(b"
") 253 | f.write(b'') 254 | f.write( 255 | ( 256 | "\n

Please choose file to upload to %s

\n" 257 | % displaypath 258 | ).encode() 259 | ) 260 | f.write(b"
") 261 | f.write( 262 | b'
' 263 | ) 264 | f.write(b"
") 265 | f.write(b"") 266 | f.write(b"") 267 | 268 | length = f.tell() 269 | f.seek(0) 270 | self.send_response(200) 271 | self.send_header("Content-type", "text/html; charset=utf-8") 272 | self.send_header("Content-Length", str(length)) 273 | self.end_headers() 274 | return f 275 | 276 | def copyfile(self, source, outputfile): 277 | shutil.copyfileobj(source, outputfile) 278 | 279 | return FileUploadServerHandler 280 | 281 | 282 | def get_ssid(): 283 | 284 | if operating_system == MacOS: 285 | ssid = ( 286 | os.popen( 287 | "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | awk '/ SSID/ {print substr($0, index($0, $2))}'" 288 | ) 289 | .read() 290 | .strip() 291 | ) 292 | return ssid 293 | 294 | elif operating_system == "Linux": 295 | ssid = os.popen("iwgetid -r 2>/dev/null",).read().strip() 296 | if not ssid: 297 | ssid = ( 298 | os.popen( 299 | "nmcli -t -f active,ssid dev wifi | egrep '^yes' | cut -d\\' -f2 | sed 's/yes://g' 2>/dev/null" 300 | ) 301 | .read() 302 | .strip() 303 | ) 304 | return ssid 305 | 306 | else: 307 | interface_info = os.popen("netsh.exe wlan show interfaces").read() 308 | for line in interface_info.splitlines(): 309 | if line.strip().startswith("Profile"): 310 | ssid = line.split(":")[1].strip() 311 | return ssid 312 | 313 | 314 | def get_local_ip(): 315 | try: 316 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 317 | s.connect(("8.8.8.8", 80)) 318 | return s.getsockname()[0] 319 | except OSError: 320 | print("Network is unreachable") 321 | clean_exit() 322 | 323 | 324 | def get_local_ips_available(): 325 | """Get a list of all local IPv4 addresses except localhost""" 326 | try: 327 | import netifaces 328 | 329 | ips = [] 330 | for iface in netifaces.interfaces(): 331 | ips.extend( 332 | [ 333 | x["addr"] 334 | for x in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []) 335 | if x and "addr" in x 336 | ] 337 | ) 338 | 339 | localhost_ip = re.compile("^127.+$") 340 | return [x for x in sorted(ips) if not localhost_ip.match(x)] 341 | 342 | except ModuleNotFoundError: 343 | pass 344 | 345 | 346 | def random_port(): 347 | return random.randint(1024, 65535) 348 | 349 | 350 | def print_qr_code(address): 351 | qr = qrcode.QRCode( 352 | version=1, 353 | error_correction=qrcode.constants.ERROR_CORRECT_L, 354 | box_size=10, 355 | border=4, 356 | ) 357 | qr.add_data(address) 358 | qr.make() 359 | 360 | qr.print_tty() 361 | 362 | 363 | def start_download_server(file_path, **kwargs): 364 | PORT = int(kwargs["custom_port"]) if kwargs.get( 365 | "custom_port") else random_port() 366 | LOCAL_IP = kwargs["ip_addr"] if kwargs["ip_addr"] else get_local_ip() 367 | SSID = get_ssid() 368 | auth = kwargs.get("auth") 369 | debug = kwargs.get("debug", False) 370 | 371 | if not os.path.exists(file_path): 372 | print("No such file or directory") 373 | clean_exit() 374 | 375 | delete_zip = 0 376 | abs_path = os.path.normpath(os.path.abspath(file_path)) 377 | file_dir = os.path.dirname(abs_path) 378 | file_path = os.path.basename(abs_path) 379 | 380 | os.chdir(file_dir) 381 | 382 | if os.path.isdir(file_path): 383 | zip_name = pathlib.PurePosixPath(file_path).name 384 | 385 | try: 386 | path_to_zip = make_archive(zip_name, "zip", file_path) 387 | file_path = os.path.basename(path_to_zip) 388 | delete_zip = file_path 389 | except PermissionError: 390 | print("Permission denied") 391 | clean_exit() 392 | 393 | file_path = file_path.replace(" ", "%20") 394 | 395 | handler = FileTransferServerHandlerClass( 396 | file_path, auth, debug, kwargs.get("no_force_download", False) 397 | ) 398 | httpd = socketserver.TCPServer(("", PORT), handler) 399 | 400 | address = "http://" + str(LOCAL_IP) + ":" + str(PORT) + "/" + file_path 401 | 402 | print("Scan the following QR code to start downloading.") 403 | if SSID: 404 | print( 405 | "Make sure that your smartphone is connected to \033[1;94m{}\033[0m".format( 406 | SSID 407 | ) 408 | ) 409 | 410 | if debug: 411 | print(address) 412 | 413 | print_qr_code(address) 414 | 415 | try: 416 | httpd.serve_forever() 417 | except KeyboardInterrupt: 418 | pass 419 | 420 | if delete_zip != 0: 421 | os.remove(delete_zip) 422 | 423 | clean_exit() 424 | 425 | 426 | def start_upload_server(file_path, debug, custom_port, ip_addr, auth): 427 | 428 | if custom_port: 429 | PORT = int(custom_port) 430 | else: 431 | PORT = random_port() 432 | 433 | if ip_addr: 434 | LOCAL_IP = ip_addr 435 | else: 436 | LOCAL_IP = get_local_ip() 437 | 438 | SSID = get_ssid() 439 | 440 | if not os.path.exists(file_path): 441 | print("No such file or directory") 442 | clean_exit() 443 | 444 | if not os.path.isdir(file_path): 445 | print("%s is not a folder." % file_path) 446 | clean_exit() 447 | 448 | handler = FileUploadServerHandlerClass(file_path, auth, debug) 449 | 450 | try: 451 | httpd = socketserver.TCPServer(("", PORT), handler) 452 | except OSError: 453 | print(message) 454 | clean_exit() 455 | 456 | address = "http://" + str(LOCAL_IP) + ":" + str(PORT) + "/" 457 | 458 | print("Scan the following QR code to start uploading.") 459 | if SSID: 460 | print( 461 | "Make sure that your smartphone is connected to \033[1;94m{}\033[0m".format( 462 | SSID 463 | ) 464 | ) 465 | 466 | if debug: 467 | print(address) 468 | 469 | print_qr_code(address) 470 | 471 | try: 472 | httpd.serve_forever() 473 | except KeyboardInterrupt: 474 | pass 475 | 476 | clean_exit() 477 | 478 | 479 | def b64_auth(a): 480 | splited = a.split(":") 481 | if len(splited) != 2: 482 | msg = "The format of auth should be [username:password]" 483 | raise argparse.ArgumentTypeError(msg) 484 | return base64.b64encode(a.encode()) 485 | 486 | 487 | def main(): 488 | if operating_system != Windows: 489 | signal.signal(signal.SIGTSTP, signal.SIG_IGN) 490 | 491 | parser = argparse.ArgumentParser( 492 | description="Transfer files over WiFi between your computer and your smartphone from the terminal" 493 | ) 494 | 495 | parser.add_argument( 496 | "file_path", 497 | action="store", 498 | help="path that you want to transfer or store the received file.", 499 | ) 500 | parser.add_argument( 501 | "--debug", "-d", action="store_true", help="show the encoded url." 502 | ) 503 | parser.add_argument( 504 | "--receive", 505 | "-r", 506 | action="store_true", 507 | help="enable upload mode, received file will be stored at given path.", 508 | ) 509 | parser.add_argument("--port", "-p", dest="port", help="use a custom port") 510 | parser.add_argument( 511 | "--ip_addr", 512 | dest="ip_addr", 513 | choices=get_local_ips_available(), 514 | help="specify IP address", 515 | ) 516 | parser.add_argument( 517 | "--auth", 518 | action="store", 519 | help="add authentication, format: username:password", 520 | type=b64_auth, 521 | ) 522 | parser.add_argument( 523 | "--no-force-download", 524 | action="store_true", 525 | help="Allow browser to handle the file processing instead of forcing it to download.", 526 | ) 527 | 528 | args = parser.parse_args() 529 | 530 | if operating_system == Windows: 531 | import colorama 532 | 533 | colorama.init() 534 | print("\033[2J", end="") 535 | 536 | cursor(False) 537 | 538 | if args.receive: 539 | start_upload_server( 540 | file_path=args.file_path, 541 | debug=args.debug, 542 | custom_port=args.port, 543 | ip_addr=args.ip_addr, 544 | auth=args.auth, 545 | ) 546 | else: 547 | start_download_server( 548 | args.file_path, 549 | debug=args.debug, 550 | custom_port=args.port, 551 | ip_addr=args.ip_addr, 552 | auth=args.auth, 553 | no_force_download=args.no_force_download, 554 | ) 555 | 556 | 557 | if __name__ == "__main__": 558 | main() 559 | --------------------------------------------------------------------------------