From 85edd5004779379ea64844706c97e076983ca354 Mon Sep 17 00:00:00 2001 From: Daniel Markstedt Date: Tue, 1 Nov 2022 16:43:24 -0700 Subject: [PATCH] Partition and format HFS/FAT volumes in the Web UI + SMB install (#946) - New "format as" option when creating new images; removing the image creation options from easyinstall - Bring in HFSer as new submodule providing the driver binaries; removing the Lido driver binary from this repo - Add SpeedTools driver option - Point to github mirror of hfdisk, since the original git server is down - While rearranging the easyinstall options, moved the CtrlBoard option up to the main section - Add an easyinstall script to configure Samba, while consolidating file sharing with Netatalk --- .gitignore | 5 +- docker/rascsi-web/Dockerfile | 2 +- easyinstall.sh | 283 +++++++++++++------------- lido-driver.img | Bin 16384 -> 0 bytes python/common/src/rascsi/file_cmds.py | 228 ++++++++++++++++++++- python/web/src/settings.py | 2 +- python/web/src/templates/index.html | 31 ++- python/web/src/web.py | 84 +++++++- python/web/tests/api/test_files.py | 56 +++++ python/web/tests/conftest.py | 2 +- 10 files changed, 534 insertions(+), 159 deletions(-) delete mode 100644 lido-driver.img diff --git a/.gitignore b/.gitignore index bdf7aae8..912c5e48 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ core __pycache__ current rascsi_interface_pb2.py -src/raspberrypi/hfdisk/ *~ messages.pot messages.mo @@ -27,3 +26,7 @@ s.sh # temporary kicad files *-backups + +# submodules +hfdisk* +mac-hard-disk-drivers diff --git a/docker/rascsi-web/Dockerfile b/docker/rascsi-web/Dockerfile index 95c5578c..106137be 100644 --- a/docker/rascsi-web/Dockerfile +++ b/docker/rascsi-web/Dockerfile @@ -28,7 +28,7 @@ RUN ./easyinstall.sh --run_choice=11 RUN ./easyinstall.sh --run_choice=13 # Setup wired network bridge -RUN ./easyinstall.sh --run_choice=6 --headless +RUN ./easyinstall.sh --run_choice=5 --headless USER root WORKDIR /home/pi diff --git a/easyinstall.sh b/easyinstall.sh index b352ccbb..4dba5c65 100755 --- a/easyinstall.sh +++ b/easyinstall.sh @@ -58,13 +58,13 @@ PYTHON_COMMON_PATH="$BASE/python/common" SYSTEMD_PATH="/etc/systemd/system" SSL_CERTS_PATH="/etc/ssl/certs" SSL_KEYS_PATH="/etc/ssl/private" -HFS_FORMAT=/usr/bin/hformat HFDISK_BIN=/usr/bin/hfdisk -LIDO_DRIVER=$BASE/lido-driver.img GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) GIT_REMOTE=${GIT_REMOTE:-origin} TOKEN="" SECRET_FILE="$HOME/.config/rascsi/rascsi_secret" +FILE_SHARE_PATH="$HOME/shared_files" +FILE_SHARE_NAME="Pi File Server" set -e @@ -106,7 +106,10 @@ function installPackages() { unar \ disktype \ libgmock-dev \ - man2html + man2html \ + hfsutils \ + dosfstools \ + kpartx } # install Debian packges for RaSCSI standalone @@ -592,98 +595,30 @@ function createDriveCustom() { createDrive "$driveSize" "$driveName" } -# Creates an HFS file system -function formatDrive() { - diskPath="$1" - volumeName="$2" - - if [ ! -x $HFS_FORMAT ]; then - # Install hfsutils to have hformat to format HFS - sudo apt-get install hfsutils --assume-yes xWDjB7tBl2L@Qg=1RQ)f!30j&K}dYzIiEg{K7T8OIn^ zV6W@ZfC%826jC-}x=hLtrArgxW;&U)!(M_@b-l(Og+Sr4RxcAjVADRt3+<}cvufrD2(2{DlI*R_h-T;YmDd?j!Hr00k{V^}UTJdC9nz{W(ZeA!${2SS@b;spQX%SX9w7mB zn{%*ir|~(;Bf;N8Ho&)NBU`fLnowJbarGf8d)OSGs@Qg@?V$3GZ%XQx(-a_D!z3X1 zlVtLDQGUQ{8~kjM`%Wqbjsb~R3$5MKeiGO0qGB?(Tr!jg`x3#n?~tH>|0z7BKg;5Y zR{&R@;nQ$c8$(3DVDtC`tl*4s$ha#VU|4k?622eb+^Fjh3jD0I=*ElvYEEdPf1 z?EH3-kxbxUq=K79KPK0e+5%pOR~qz>NMT@tJf!YmUqh-iA1if6wcuH%7T7LAbG=x# zgVHL+#Ae z6j@{sTDR@X$F4sGJtIc1T-5ED%SFbj{1~}i(tgOYNG)n>dOb}g7aMoK7kRnml5%6) z*A-gIM}BQ{NrUROfUnXeh3$7cn#ja@gMBqlD(V)Rd~H4%Ya)AJz>27%Y*vlR^_rN$ zc&<0t2g`#Us+u9WriK((V%^B>mT3g_(+#PTXR`GtF%CKrX5-2lQgl7r4tquWFm8Xg z9t}($j{8#FJ8lOlJ=oRB zWFHnnWci?`l8Twu*}N~nE`?3*7)cR=MH3U%^0M&N5Tz}!<`=QAT=SlU>3?Lj-?RDa z+RoXe?;F@Pac#_jU5P!b?c4di6XmQvGP50&w*}r**!=IApERUCQ)_$5v4axWQ?P}B z$;+dTSQ%rt%->BmK$k@ZqO?y0l#DUb;xTJu9K7wqQ9^2Vlrjn)=CH8+TVU2P32$JG z2wgqF=mom@>2v*V7oRd>i^J0S9kCS}2Ww-n12*g@8}^gP_)#pVSy3}NVxx<=)a^qS zfB!5Gmq(d&HgB>nkdGr~$YKY*S>AQoV3tDN@;$^$ceZYt#{ucL>GnyS&dS+Bq9azA z#be3h$?`CV%W!QLM|?DN9$wpu!lIZvm``#q1AR%2v>x@PmbU`dNXCRm{UU13bF~)K zHq6y3P^+J-IW@@7p)QaBNHhEoVk2b>-jI)+#4o@fczY`iLhl`lZ z!vgEU7Z)zUmsPRgTbOG#XkS*$h10bTV^;Tj7D%L<60p~>+wi((<~5)dCG(ocbdI0M z>f9^915K+oGI;kz;(S*rAMZSC4BPZ0;Z96f7AeyZVh0GC3Y1nas@JL4Ojko~y{fB3HwNLWbJe8dt!q zph}#`=7aB3=i|HU#x7W1KOf)IYdXSa(_8vaBXrs{!VCzV-c9r-!;0yh?j`!!g$tHs z57AFT^gBU$$!>W4$oJ!@pZ_DUXBNOd4%i-yeKbTjURE9k{|@lkNZg)&w47Hc6VdVY ze$LiSW*MH5acDC<9v?sY_R&{=Am@z_zddZDMAgqn&=1+H^pf+{=g1r(1q=DfP-7Qv z`v^XF0p#VQ6Gz`T_~gM#?9WUFeB2S{W88(I9-%T%VS2Gzmsi*W`X|*g#%Z{i@uKs$ z+XS57dj8OECVhtwp4vS{L-00-5#jUULWV&xY3*qzIU4A+{G;*5L}7tB~%KL%lR}`6e0j zkzZ;2lQ6>Os6|fDLB!vsfyuHmC%HHeUlDL_^sDPV#3h$EZ^fx)cs;y5>U_37P;FW- zqHfUY9nD8ncl9mJM;fw^+t!jy4nd`$$$)oqzU(EcH=-O8M0=C zhN2lA`r|1L+Sz^tmZlIAxgqLy$;{+s%maUB=uf$BGt@RC><*JRT{Ihj1s*xe*Ep@{|T+VeEc1}#T%MrAp;E~_8`N^&MW^H8GLL9 z?gtpPPndcUT)| z$HFF`VbO)o^DHiMF~40xRfuo2(~IW+ndWm{h>xHN5Q~{bJLwwTcW0x1q92BybHs3d zVzE^>6*FmSo|p8PBh72lKcf(xnuB>PFsY2f_GIHRc*a}2@Qmok2|+cYo|l;Slt2z# z(DUD)2W=6$PSg^4Um*&>CyX&%H_b*^I2(X3o(+rkFq7YRF>LUqWNTS7%K2qPnQL&i zk_U|Gz~rq3!4oZ)A|Ei%exM$GmTF`55_>?i_JZt9U{6aXwM6zJ7hp%Jz^U80EJlxz zF~TK|o-McSkT>5ye70QPaTXROHWR_wj7d}qi*e|K@!7IF0%WlV92+IGtIjkAoiEq& zBrfcLinWbYX@@pSVboTlI8dD0+6?)o_-RK#oDR`{za|gx-$lO42(QxD%X|&X{kYUJ z^E$5DL}ci(V8k1?#C9at9qMs3&jRri+KN+!0Myidl_f}$i&-#Z_ zoz+~XD;#ts8+BsEN(evb4wE*6gC4L5nPZav-3&N&$~%_BI!IRNgP)x%fveQNO?L6q zu#C5=MmZhlIKufijYANz$X_kdVt;nxT&5N%*X3WbKmRF(W?CZG@WlFmG!~-i0_X%E z>|;_wY;8mlbV=#wZ5H#hbNWYL(#Nrry4!5>Hnyojx09POdG8xhiD&U7o%Y)5Zdv#I zLBup>oFoq}_pEcCT8WdhucdJY%0|G=akUUL89<4l3}c1t3d`Kt z7tljif^qm^be=;;D>=cJKm&LhDdyjevl`YWlxXQO4 z>!LxP+D@JgnXv50~EhBg75e$k!p4=D3^U zFR?Gt(GBXuZFj$Pj)XnWA>ujV*hPY5bh>Cjh5WdBP>vnM4DSUWEO!ycnrgW!dauc` z9hs^!Jzq4J#kgX>2QuG9CP~24D3O+*wKF(y;9+s47U||=uL^GuBYP!ivnzc5?r=fn zltd^(8#D&H0X39tEim~3&U?`4?YcatZA@omX*5m7S4SPK5;2K_DkQ_?GR?!5+#2kQ z(I8bckEOYn%uZxn!`Oj&VOR5!aJ~zBbo|SK#_=y#=iBdbH0p4_eZ{^*vyb)iL*lf8 zi21z=nT^~Q@Q!bLGwx`_tes?<(Idd9#4Cmr*J)ia4TJTiNW(=EVWZA^| zjMi_9^^@)=!Cg-S4l%hn4GB!c0t6zaM3wF@0+Vq>Hx!V~S5C>JvU%#1mYLB`PV8)L zSigQfbu7?iNPd=|>t}(t12Z|sidEQ3j5p!a9a%OFec2T5|*-m77*}kB26v1;RMXO;Qs4V$_FC~__ zM<}|cnhvMur3*Nyou^JXV+RI>CzDSD>TDLuHV0L(0P3yR7lrtJke$O|G|D|n%K1O*oi>H^_3hdGP7mLHjcHw(Qc%9){e1Sp!Vs>u<597V9 z!E^}Zxn39gt|brS4D3mKVZr!g7>{vU3!_aL3r4=VXe9P2+YMnO+aKpFNeQkd77han$We9R24eTfpr} zKJ{$V_g==FCs=-ZGJd<4YP6`uLkIk)Hou%nvl{NBoFpRVj3aV|ZjhZtp8jk+irz}x zMh)_VWXG5zdR#i&%c50`cus>i=zZ{MEs|wz7qv>ow|CK5a^m@Ix)WJfoP(b&lZ|RC ze5lFVOvTI!` zg#~1YkiF1P|MDIZs-CiUv6)I{Z4~H7Y5MC>7Y_i=W|i+V4EQ;TrA zK;9PlKP}VEywueJ#VyxSkdNa+gjm#g^>x(iQAbd#)qjk76(2`cuYVtLGH#1AJ#)9g zxUB~5{C~wS%P|}LSNR>lJx3gN=#$E^1?_CyUxnaJNElwY{7?NIxQYH6eQC8j8Qju9 z+bZRG)bdjHz4)FcFuA_IAvIHCJEEOiace5q&Agxnvayq)r_vM$kav$U;`OCw5bd!$ zSp6*e>gfDQl$}%6LX7B_v$wPjsgEz(h9mM=<2q{cHl{wlP#aJD{ktvp0j|^@^^&d4 zm-_gOjN5^?YHh=Jwc4>FepR!Nn;KGYRebGE%7=9*OwgwHsZ}j>3-1J) zyoq{W>ethWa)oHRqRsvg;T~ynAFNR|bj6Lb2Bk)>`}hjeGs=Vms8^$ZVc#pPFGJx< z)`#Lo|18{h(U&0#vs3gOPW0EXeE_TO#fh>BQ7O^R+5eD0YX?pX>=wEOXTvYzTUJ>v z%z=GT6(ZbkqGdTO7(0)?i*q7IifFTi=!2B@zY&GD!Ss;vH-!J#xQkh1+zNsIO0-7i zTA};MMWlm$m{;zXlR!=aISJ$>kdr`80yzofB#@IpP69azkdr`80yzofB#@K9-?RiI@82~0Ip+UD3H%Q-y{t6= diff --git a/python/common/src/rascsi/file_cmds.py b/python/common/src/rascsi/file_cmds.py index 0cee0035..b1735df1 100644 --- a/python/common/src/rascsi/file_cmds.py +++ b/python/common/src/rascsi/file_cmds.py @@ -8,11 +8,12 @@ from os import path, walk from functools import lru_cache from pathlib import PurePath, Path from zipfile import ZipFile, is_zipfile -from subprocess import run, CalledProcessError +from subprocess import run, Popen, PIPE, CalledProcessError, TimeoutExpired from json import dump, load from shutil import copyfile from urllib.parse import quote from tempfile import TemporaryDirectory +from re import search import requests @@ -366,6 +367,222 @@ class FileCmds: } + # noinspection PyMethodMayBeStatic + def partition_disk(self, file_name, volume_name, disk_format): + """ + Creates a partition table on an image file. + Takes (str) file_name, (str) volume_name, (str) disk_format as arguments. + disk_format is either HFS or FAT + Returns (dict) with (bool) status, (str) msg + """ + server_info = self.ractl.get_server_info() + full_file_path = Path(server_info["image_dir"]) / file_name + + # Inject hfdisk commands to create Drive with correct partitions + # https://www.codesrc.com/mediawiki/index.php/HFSFromScratch + # i initialize partition map + # continue with default first block + # C Create 1st partition with type specified next) + # continue with default + # 32 32 blocks (required for HFS+) + # Driver_Partition Partition Name + # Apple_Driver Partition Type (available types: Apple_Driver, + # Apple_Driver43, Apple_Free, Apple_HFS...) + # C Create 2nd partition with type specified next + # continue with default first block + # continue with default block size (rest of the disk) + # ${volumeName} Partition name provided by user + # Apple_HFS Partition Type + # w Write partition map to disk + # y Confirm partition table + # p Print partition map + if disk_format == "HFS": + partitioning_tool = "hfdisk" + commands = [ + "i", + "", + "C", + "", + "32", + "Driver_Partition", + "Apple_Driver", + "C", + "", + "", + volume_name, + "Apple_HFS", + "w", + "y", + "p", + ] + # Create a DOS label, primary partition, W95 FAT type + elif disk_format == "FAT": + partitioning_tool = "fdisk" + commands = [ + "o", + "n", + "p", + "", + "", + "", + "t", + "b", + "w", + ] + try: + process = Popen( + [partitioning_tool, str(full_file_path)], + stdin=PIPE, + stdout=PIPE, + ) + for command in commands: + process.stdin.write(bytes(command + "\n", "utf-8")) + process.stdin.flush() + try: + outs, errs = process.communicate(timeout=15) + if outs: + logging.info(str(outs, "utf-8")) + if errs: + logging.error(str(errs, "utf-8")) + if process.returncode: + self.delete_file(Path(file_name)) + return {"status": False, "msg": errs} + except TimeoutExpired: + process.kill() + outs, errs = process.communicate() + if outs: + logging.info(str(outs, "utf-8")) + if errs: + logging.error(str(errs, "utf-8")) + self.delete_file(Path(file_name)) + return {"status": False, "msg": errs} + + except (OSError, IOError) as error: + logging.error(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8")) + self.delete_file(Path(file_name)) + return {"status": False, "msg": error.stderr.decode("utf-8")} + + return {"status": True, "msg": ""} + + + # noinspection PyMethodMayBeStatic + def format_hfs(self, file_name, volume_name, driver_path): + """ + Initializes an HFS file system and injects a hard disk driver + Takes (str) file_name, (str) volume_name and (Path) driver_path as arguments. + Returns (dict) with (bool) status, (str) msg + """ + server_info = self.ractl.get_server_info() + full_file_path = Path(server_info["image_dir"]) / file_name + + try: + run( + [ + "dd", + f"if={driver_path}", + f"of={full_file_path}", + "seek=64", + "count=32", + "bs=512", + "conv=notrunc", + ], + capture_output=True, + check=True, + ) + except (FileNotFoundError, CalledProcessError) as error: + logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8")) + self.delete_file(Path(file_name)) + return {"status": False, "msg": error.stderr.decode("utf-8")} + + try: + process = run( + [ + "hformat", + "-l", + volume_name, + str(full_file_path), + "1", + ], + capture_output=True, + check=True, + ) + logging.info(process.stdout.decode("utf-8")) + except (FileNotFoundError, CalledProcessError) as error: + logging.error(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8")) + self.delete_file(Path(file_name)) + return {"status": False, "msg": error.stderr.decode("utf-8")} + + return {"status": True, "msg": ""} + + + # noinspection PyMethodMayBeStatic + def format_fat(self, file_name, volume_name, fat_size): + """ + Initializes a FAT file system + Takes (str) file_name, (str) volume_name and (str) FAT size (12|16|32) as arguments. + Returns (dict) with (bool) status, (str) msg + """ + server_info = self.ractl.get_server_info() + full_file_path = Path(server_info["image_dir"]) / file_name + loopback_device = "" + + try: + process = run( + ["kpartx", "-av", str(full_file_path)], + capture_output=True, + check=True, + ) + logging.info(process.stdout.decode("utf-8")) + if process.returncode == 0: + loopback_device = search(r"(loop\d\D\d)", process.stdout.decode("utf-8")).group(1) + else: + logging.info(process.stdout.decode("utf-8")) + self.delete_file(Path(file_name)) + return {"status": False, "msg": error.stderr.decode("utf-8")} + except (FileNotFoundError, CalledProcessError) as error: + logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8")) + self.delete_file(Path(file_name)) + return {"status": False, "msg": error.stderr.decode("utf-8")} + + args = [ + "mkfs.fat", + "-v", + "-F", + fat_size, + "-n", + volume_name, + "/dev/mapper/" + loopback_device, + ] + try: + process = run( + args, + capture_output=True, + check=True, + ) + logging.info(process.stdout.decode("utf-8")) + except (FileNotFoundError, CalledProcessError) as error: + logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8")) + self.delete_file(Path(file_name)) + return {"status": False, "msg": error.stderr.decode("utf-8")} + + try: + process = run( + ["kpartx", "-dv", str(full_file_path)], + capture_output=True, + check=True, + ) + logging.info(process.stdout.decode("utf-8")) + if process.returncode: + logging.info(process.stderr.decode("utf-8")) + logging.warning("Failed to delete loopback device. You may have to do it manually") + except (FileNotFoundError, CalledProcessError) as error: + logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8")) + self.delete_file(Path(file_name)) + return {"status": False, "msg": error.stderr.decode("utf-8")} + + return {"status": True, "msg": ""} + + def download_file_to_iso(self, url, *iso_args): """ Takes (str) url and one or more (str) *iso_args @@ -446,9 +663,12 @@ class FileCmds: headers={"User-Agent": "Mozilla/5.0"}, ) as req: req.raise_for_status() - with open(f"{save_dir}/{file_name}", "wb") as download: - for chunk in req.iter_content(chunk_size=8192): - download.write(chunk) + try: + with open(f"{save_dir}/{file_name}", "wb") as download: + for chunk in req.iter_content(chunk_size=8192): + download.write(chunk) + except FileNotFoundError as error: + return {"status": False, "msg": str(error)} except requests.exceptions.RequestException as error: logging.warning("Request failed: %s", str(error)) return {"status": False, "msg": str(error)} diff --git a/python/web/src/settings.py b/python/web/src/settings.py index 5ce3fea4..f33fc52a 100644 --- a/python/web/src/settings.py +++ b/python/web/src/settings.py @@ -8,7 +8,7 @@ import rascsi.common_settings WEB_DIR = getcwd() HOME_DIR = "/".join(WEB_DIR.split("/")[0:3]) -AFP_DIR = f"{HOME_DIR}/afpshare" +FILE_SERVER_DIR = f"{HOME_DIR}/shared_files" MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", str(1024 * 1024 * 1024 * 4)) # 4gb diff --git a/python/web/src/templates/index.html b/python/web/src/templates/index.html index 96246f43..d57a57d9 100644 --- a/python/web/src/templates/index.html +++ b/python/web/src/templates/index.html @@ -439,7 +439,7 @@
  • {{ _("The largest file size accepted in this form is %(max_file_size)s MiB. Use other file transfer means for larger files.", max_file_size=max_file_size) }}
  • {{ _("File uploads will progress only if you stay on this page. If you navigate away before the transfer is completed, you will end up with an incomplete file.") }}
  • -
  • {{ _("Install Netatalk to use the AFP File Server.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}
  • +
  • {{ _("Install Netatalk or Samba to use the File Server.") }}
@@ -447,8 +447,8 @@

@@ -489,15 +489,15 @@ {{ _("Download File from the Web") }}
    -
  • {{ _("Install Netatalk to use the AFP File Server.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}
  • +
  • {{ _("Install Netatalk or Samba to use the File Server.") }}
@@ -560,6 +560,7 @@
  • {{ _("Please refer to wiki documentation to learn more about the supported image file types.", url="https://github.com/akuker/RASCSI/wiki/Supported-Device-Types#image-types") }}
  • +
  • {{ _("It is not recommended to use the Lido hard disk driver with the Macintosh Plus.") }}
@@ -587,6 +588,24 @@ {% endfor %} + +
diff --git a/python/web/src/web.py b/python/web/src/web.py index 6e79f405..2565d1c7 100644 --- a/python/web/src/web.py +++ b/python/web/src/web.py @@ -59,7 +59,7 @@ from web_utils import ( ) from settings import ( WEB_DIR, - AFP_DIR, + FILE_SERVER_DIR, MAX_FILE_SIZE, DEFAULT_CONFIG, DRIVE_PROPERTIES_FILE, @@ -251,7 +251,7 @@ def index(): drive_properties=format_drive_properties(APP.config["RASCSI_DRIVE_PROPERTIES"]), RESERVATIONS=RESERVATIONS, CFG_DIR=CFG_DIR, - AFP_DIR=AFP_DIR, + FILE_SERVER_DIR=FILE_SERVER_DIR, PROPERTIES_SUFFIX=PROPERTIES_SUFFIX, ARCHIVE_FILE_SUFFIXES=ARCHIVE_FILE_SUFFIXES, CONFIG_FILE_SUFFIX=CONFIG_FILE_SUFFIX, @@ -864,8 +864,8 @@ def download_file(): """ destination = request.form.get("destination") url = request.form.get("url") - if destination == "afp": - destination_dir = AFP_DIR + if destination == "file_server": + destination_dir = FILE_SERVER_DIR else: server_info = ractl_cmd.get_server_info() destination_dir = server_info["image_dir"] @@ -895,8 +895,8 @@ def upload_file(): return make_response(auth["msg"], 403) destination = request.form.get("destination") - if destination == "afp": - destination_dir = AFP_DIR + if destination == "file_server": + destination_dir = FILE_SERVER_DIR else: server_info = ractl_cmd.get_server_info() destination_dir = server_info["image_dir"] @@ -913,6 +913,7 @@ def create_file(): size = (int(request.form.get("size")) * 1024 * 1024) file_type = request.form.get("type") drive_name = request.form.get("drive_name") + drive_format = request.form.get("drive_format") safe_path = is_safe_path(file_name) if not safe_path["status"]: @@ -922,6 +923,68 @@ def create_file(): if not process["status"]: return response(error=True, message=process["msg"]) + message_postfix = "" + + # Formatting and injecting driver, if one is choosen + if drive_format: + volume_name = f"HD {size / 1024 / 1024:0.0f}M" + known_formats = [ + "Lido 7.56", + "SpeedTools 3.6", + "FAT16", + "FAT32", + ] + message_postfix = f" ({drive_format})" + + if drive_format not in known_formats: + return response( + error=True, + message=_( + "%(drive_format)s is not a valid hard disk format.", + drive_format=drive_format, + ) + ) + elif drive_format.startswith("FAT"): + if drive_format == "FAT16": + fat_size = "16" + elif drive_format == "FAT32": + fat_size = "32" + else: + return response( + error=True, + message=_( + "%(drive_format)s is not a valid hard disk format.", + drive_format=drive_format, + ) + ) + + process = file_cmd.partition_disk(full_file_name, volume_name, "FAT") + if not process["status"]: + return response(error=True, message=process["msg"]) + + process = file_cmd.format_fat( + full_file_name, + # FAT volume labels are max 11 chars + volume_name[:11], + fat_size, + ) + if not process["status"]: + return response(error=True, message=process["msg"]) + + else: + driver_base_path = Path(f"{WEB_DIR}/../../../mac-hard-disk-drivers") + process = file_cmd.partition_disk(full_file_name, volume_name, "HFS") + if not process["status"]: + return response(error=True, message=process["msg"]) + + process = file_cmd.format_hfs( + full_file_name, + volume_name, + driver_base_path / Path(drive_format.replace(" ", "-") + ".img"), + ) + if not process["status"]: + return response(error=True, message=process["msg"]) + # Creating the drive properties file, if one is chosen if drive_name: properties = get_properties_by_drive_name( @@ -937,15 +1000,20 @@ def create_file(): return response( status_code=201, message=_( - "Image file with properties created: %(file_name)s", + "Image file with properties created: %(file_name)s%(drive_format)s", file_name=full_file_name, + drive_format=message_postfix, ), image=full_file_name, ) return response( status_code=201, - message=_("Image file created: %(file_name)s", file_name=full_file_name), + message=_( + "Image file created: %(file_name)s%(drive_format)s", + file_name=full_file_name, + drive_format=message_postfix, + ), image=full_file_name, ) diff --git a/python/web/tests/api/test_files.py b/python/web/tests/api/test_files.py index b36d7f5f..4ad2a4df 100644 --- a/python/web/tests/api/test_files.py +++ b/python/web/tests/api/test_files.py @@ -65,6 +65,62 @@ def test_create_file_with_properties(http_client, list_files, delete_file): delete_file(file_name) +# route("/files/create", methods=["POST"]) +def test_create_file_and_format_hfs(http_client, list_files, delete_file): + file_prefix = str(uuid.uuid4()) + file_name = f"{file_prefix}.hda" + + response = http_client.post( + "/files/create", + data={ + "file_name": file_prefix, + "type": "hda", + "size": 1, + "drive_format": "Lido 7.56", + }, + ) + + response_data = response.json() + + assert response.status_code == 201 + assert response_data["status"] == STATUS_SUCCESS + assert response_data["data"]["image"] == file_name + assert response_data["messages"][0]["message"] == f"Image file created: {file_name} (Lido 7.56)" + assert file_name in list_files() + + # Cleanup + delete_file(file_name) + + +# route("/files/create", methods=["POST"]) +def test_create_file_and_format_fat(env, http_client, list_files, delete_file): + if env["is_docker"]: + pytest.skip("Test not supported in Docker environment.") + file_prefix = str(uuid.uuid4()) + file_name = f"{file_prefix}.hdr" + + response = http_client.post( + "/files/create", + data={ + "file_name": file_prefix, + "type": "hdr", + "size": 1, + "drive_format": "FAT32", + }, + ) + + response_data = response.json() + + assert response.status_code == 201 + assert response_data["status"] == STATUS_SUCCESS + assert response_data["data"]["image"] == file_name + assert response_data["messages"][0]["message"] == f"Image file created: {file_name} (FAT32)" + assert file_name in list_files() + + # Cleanup + delete_file(file_name) + + # route("/files/rename", methods=["POST"]) def test_rename_file(http_client, create_test_image, list_files, delete_file): original_file = create_test_image(auto_delete=False) diff --git a/python/web/tests/conftest.py b/python/web/tests/conftest.py index 7e70b696..3fdeb92d 100644 --- a/python/web/tests/conftest.py +++ b/python/web/tests/conftest.py @@ -23,7 +23,7 @@ def env(pytestconfig): "home_dir": home_dir, "cfg_dir": f"{home_dir}/.config/rascsi", "images_dir": f"{home_dir}/images", - "afp_dir": f"{home_dir}/afpshare", + "file_server_dir": f"{home_dir}/shared_files", }