.
675 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include src/ventoyl/etc/url_config.json
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://svgshare.com/i/Zhy.svg)
2 | [](https://www.python.org/)
3 | [](https://pypi.python.org/pypi/ventoyu/)
4 | [](https://pypi.python.org/pypi/ventoyu/)
5 |
6 | # Ventoy Updater
7 |
8 |
9 | _ __ __ __ __
10 | | | / /__ ____ / /_____ __ __/ / / /
11 | | | / / _ \/ __ \/ __/ __ \/ / / / / / /
12 | | |/ / __/ / / / /_/ /_/ / /_/ / /_/ /
13 | |___/\___/_/ /_/\__/\____/\__, /\____/
14 | /____/
15 |
16 |
17 |
18 | Ventoy is a tool that allows you to create a bootable USB drive for multiple ISO files.
19 | Just copy your ISO files onto the drive and boot it.
20 |
21 | __ventoyu__ is a utility that can be used to install or even update ISO files on the USB drive.
22 | Besides ventoyu also contains features like set up a new USB drive with the Ventoy software, manage ISO sources or modify Ventoy attributes on your USB drive.
23 |
24 | ventoyu contains a configuration file (JSON format) with all sources where ISO files can be downloaded.
25 |
26 | The __ventoyl__ class can help you to talk to your Ventoy device using Python. More information here: [Wiki: ventoyLIB](https://github.com/mawigh/ventoyu/wiki/ventoyLIB)
27 |
28 | ---
29 |
30 | You will find more information about the Ventoy software here:
31 |
32 | Official Ventoy Website:
33 | https://www.ventoy.net/en/index.html
34 |
35 | Ventoy on GitHub:
36 | https://github.com/ventoy/Ventoy
37 |
38 | # Table of Contents
39 | - [Ventoy Updater](#ventoy-updater)
40 | - [Table of Contents](#table-of-contents)
41 | - [Description](#description)
42 | - [OS Support](#os-support)
43 | - [Install Ventoy Updater](#install-ventoy-updater)
44 | - [How to](#how-to)
45 | - [Install new images](#install-new-images)
46 | - [Add a new ISO download source](#add-a-new-iso-download-source)
47 | - [Install or update Ventoy](#install-or-update-ventoy)
48 | - [Get help](#get-help)
49 |
50 | ---
51 |
52 | ## Description
53 |
54 | The Ventoy Updater can be used to install new, update or remove ISO files on the Ventoy device.
55 |
56 |
57 |
58 |
59 |
60 | ## OS Support
61 |
62 | Currently supported images:
63 |
64 | | Operating system | URL |
65 | | ---------------- | --- |
66 | | Debian (arm64) | https://cdimage.debian.org/debian-cd/current/arm64/iso-cd/ |
67 | | Debian (amd64) | https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/ |
68 | | CentOS 8 | http://ftp.halifax.rwth-aachen.de/centos/8/isos/ |
69 | | Rocky Linux 8 | https://download.rockylinux.org/pub/rocky/8/isos/ |
70 | | Grml | https://download.grml.org/ |
71 | | Ubuntu (18.04, 20.04, 21.04) | https://ftp.halifax.rwth-aachen.de/ubuntu-releases/ |
72 | | Manjaro (XFCE, KDE) | https://manjaro.org/downloads/official/ |
73 | | Finnix 123 | https://www.finnix.org/releases/123/ |
74 |
75 | With the option `add-url` you have the possibility to add new ISO sources.
76 |
77 | ## Install Ventoy Updater
78 |
79 | ```bash
80 | $ pip3 install ventoyu
81 | ```
82 |
83 | **Note:** You may want to install with option `--user`.
84 |
85 | ## How to
86 |
87 | ### Install new images
88 |
89 | ```bash
90 | $ sudo ventoyu install
91 | ```
92 |
93 | ### Add a new ISO download source
94 |
95 | **Tip:** Before add a new URL, check it with `check-url`.
96 |
97 | ```bash
98 | $ sudo ventoyu add-url --url
99 | ```
100 |
101 | ## Install or update Ventoy
102 |
103 | ```bash
104 | $ sudo ventoyu --device /dev/sdX config install
105 | ```
106 |
107 | This function downloads the latest Ventoy release.
108 |
109 | **Tip:** You have the possibility to open the Ventoy GUI installer with `--gui`.
110 |
111 | ### Get help
112 |
113 | For more information type `ventoyu --help`.
114 |
--------------------------------------------------------------------------------
/bin/ventoyu:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Author: Marcel-Brian Wilkowsky (mawigh)
3 |
4 | import argparse;
5 | import os;
6 | import re;
7 | import sys;
8 | import json;
9 | import time;
10 | import threading;
11 | import queue;
12 | import shutil;
13 | import requests;
14 | import ventoyl;
15 | from bs4 import BeautifulSoup;
16 | from itertools import chain;
17 | from difflib import SequenceMatcher;
18 |
19 | def load (msg:str):
20 | characters = "/-\|";
21 | for char in characters:
22 | sys.stdout.write("\r"+msg+"... "+char);
23 | time.sleep(.1);
24 | sys.stdout.flush();
25 |
26 | def hr_filesize (size):
27 | units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
28 | for unit in units:
29 | if size < 1024.0 or unit == "PiB":
30 | break;
31 | size /= 1024.0
32 | return f"{size:.0f} {unit}";
33 |
34 | def download_file (mount_dir, iso_name, url):
35 |
36 | print("Downloading " + bchar.HEADER + str(iso_name) + bchar.ENDC + " from " + bchar.OKBLUE + str(url) + bchar.ENDC + " ...");
37 | download_request = requests.get(url + str(iso_name), stream=True, allow_redirects=True);
38 | start = time.process_time();
39 | download_length = download_request.headers.get("content-length");
40 | dl = 0;
41 | final_location = str(mount_dir) + "/" + str(iso_name);
42 | downloaded_file = open(final_location, "wb");
43 |
44 | for chunk in download_request.iter_content(1024):
45 | dl += len(chunk);
46 | downloaded_file.write(chunk);
47 | done = int(50 * dl / int(download_length));
48 | progress = "=" * done;
49 | half = " " * (50-done);
50 | timep = dl//(time.process_time() - start);
51 | timep = hr_filesize(timep/8);
52 | sys.stdout.write("\r"+bchar.OKCYAN+"["+bchar.ENDC + bchar.OKBLUE + str(progress) + str(half) +bchar.OKCYAN+"]"+bchar.ENDC+" "+str(timep));
53 |
54 | print("\nTime Elapsed: " + str(time.process_time() - start) + "s\n");
55 | downloaded_file.close();
56 |
57 | class bchar:
58 | HEADER = '\033[95m'
59 | OKBLUE = '\033[94m'
60 | OKCYAN = '\033[96m'
61 | OKGREEN = '\x1b[32m'
62 | WARNING = '\033[93m'
63 | FAIL = '\033[91m'
64 | ENDC = '\033[0m'
65 | BOLD = '\033[1m'
66 | UNDERLINE = '\033[4m'
67 |
68 | def main ():
69 |
70 | global etcdir;
71 | etcdir = os.path.abspath(os.path.join(os.path.dirname(ventoyl.__file__), "etc"));
72 | parser = argparse.ArgumentParser(prog="Ventoy Updater", description="Update ISO image found on a ventoy device");
73 | parser.add_argument("--device", help="explicit specifiy a Ventoy device (e.g. sda1)");
74 | parser.add_argument("-v", "--verbose", dest="v", action="store_true", help="enable verbose mode");
75 | parser.add_argument("-l", "--no-logo", dest="l", action="store_true", help="disable Ventoy ASCII logo");
76 | parser.add_argument("-V", "--version", action="version", version="%(prog)s 0.4");
77 |
78 | subparser = parser.add_subparsers(dest="command");
79 |
80 | sinstall = subparser.add_parser("install", help="install new ISO images");
81 | sinstall.add_argument("-m", "--no-umount", dest="m", action="store_true", help="DO NOT umount Ventoy device after install");
82 | sinstall.add_argument("-o", "--os", dest="short_name", help="short name of the OS you want to install (e.g. debian/manjaro)");
83 | sinstall.add_argument("-f", "--file", dest="file", help="copy specified ISO file to your Ventoy device (just a cp)");
84 |
85 | sremove = subparser.add_parser("remove", help="remove an installed ISO image from your Ventoy device");
86 |
87 | scheckurl = subparser.add_parser("check-url", description="If you leave the options empty, you will be asked interactively.", help="check an URL and return found ISO images");
88 | scheckurl.add_argument("--url", dest="url", help="the URL you want to check.", required=False);
89 |
90 | saddurl = subparser.add_parser("add-url", description="If you leave the options empty, you will be asked interactively.", help="add URL OS ISO sources to the JSON config file ("+ str(etcdir) +"/url_config.json)");
91 | saddurl.add_argument("--url", dest="addurl", help="the URL you want to add.", required=False);
92 | saddurl.add_argument("--short-name", dest="sname", help="the short name of your new entry. Like 'debian' or 'ubuntu'", required=False);
93 | saddurl.add_argument("--long-name", dest="lname", help="the long name of your added entry. Like 'Debian Buster' or 'Ubuntu Bionic Beaver'", required=False);
94 |
95 | scupdate = subparser.add_parser("check-update", help="Check for updates for your installed ISO images");
96 |
97 | supdate = subparser.add_parser("update", help="update all or specific ISO images");
98 | supdate.add_argument("--all", action="store_true", help="try to update all ISO images");
99 |
100 | slist = subparser.add_parser("list", help="list installed ISO images");
101 | sstats = subparser.add_parser("stats", help="print usage statistics");
102 |
103 | sconfig = subparser.add_parser("config", help="Modify your Ventoy device. Type config --help for more information");
104 | sconfigsubparser = sconfig.add_subparsers(dest="configcommand");
105 |
106 | sconfiginstall = sconfigsubparser.add_parser("install", help="Install/Update latest Ventoy software on your device");
107 | sconfiginstall.add_argument("--gui", action="store_true", help="Opens the Ventoy GUI installer");
108 | sconfiginstall.add_argument("-f", "--force", action="store_true", help="Force the installation with the shell installer");
109 |
110 | sconfigmodify = sconfigsubparser.add_parser("modify", help="Modify Ventoy attributes/plugins on your Ventoy device");
111 | sconfigmodify.add_argument("-f", "--force", action="store_true", help="Force the ...");
112 |
113 | smodifysubparser = sconfigmodify.add_subparsers(dest="modifycommand");
114 |
115 | smodifytheme = smodifysubparser.add_parser("theme", description="Install GRUB Themes to your Ventoy drive", help="Changing a Ventoy theme");
116 |
117 | slastlog = subparser.add_parser("lastlog", help="Output the last (10 lines) log information ("+str(etcdir)+"/ventoyl.log)");
118 | slastlog.add_argument("-l", "--lines", help="Specify how may lines should be printed to the output (default is 10 lines)");
119 |
120 | args = parser.parse_args();
121 |
122 | url_file = open(etcdir + "/url_config.json", "r");
123 | global json_data;
124 | json_data = json.load(url_file);
125 |
126 | global verbose_mode;
127 | verbose_mode = False;
128 | if args.v:
129 | verbose_mode = True;
130 |
131 | global ventoy;
132 |
133 | if not args.l:
134 | print(ventoyu_ascii);
135 | check_root();
136 | if args.device:
137 | try:
138 | ventoy = ventoyl.ventoyl(str(args.device), verbose_mode);
139 | except OSError:
140 | ventoy = None;
141 | else:
142 | ventoy = ventoyl.ventoyl(None, verbose_mode);
143 |
144 | #if ventoy:
145 | # if not ventoy.ventoy_device:
146 | # sys.exit( bchar.FAIL + "Error: No Ventoy device found. Plug in your Ventoy device or specify it explicity with --device");
147 | #else:
148 | # sys.exit( bchar.FAIL + "Error: Are you sure your specified Ventoy device ("+str(args.device)+") exists? Please have a look.");
149 | if args.command == "stats":
150 | print_stats();
151 | if args.command == "install":
152 | umount = True;
153 | short_name = "";
154 | if args.m:
155 | umount = False;
156 | if args.short_name:
157 | short_name = args.short_name;
158 | install(umount, short_name);
159 | if args.command == "list":
160 | listfiles();
161 | if args.command == "update":
162 | update_iso();
163 | if args.command == "check-update":
164 | #load_wrapper("Checking for updates", check_update);
165 | check_update();
166 | if args.command == "remove":
167 | remove_iso();
168 | if args.command == "check-url":
169 | if args.url:
170 | checkurl(True, args.url);
171 | else:
172 | checkurl(False);
173 | if args.command == "add-url":
174 | print(args);
175 | if args.sname:
176 | s_name = args.sname;
177 | else:
178 | s_name = None;
179 | if args.lname:
180 | l_name = args.lname;
181 | else:
182 | l_name = None;
183 | if args.addurl:
184 | newurl = args.addurl;
185 | else:
186 | newurl = None;
187 |
188 | addurl(newurl, s_name, l_name);
189 | if args.command == "config":
190 | subaction = "";
191 | if args.configcommand == "modify":
192 | subaction = args.modifycommand;
193 | action = "modify";
194 | if args.configcommand == "install":
195 | if not args.device and not args.gui:
196 | sys.exit(bchar.FAIL + "Error: You have to explicity set your device with --device or start the GUI installer with --gui" + bchar.ENDC);
197 | action = "install";
198 | if args.gui:
199 | action = "opengui";
200 | if not args.configcommand:
201 | sconfig.print_help();
202 | sys.exit();
203 | config(action, args.force, subaction);
204 | if args.command == "lastlog":
205 | nlines = 10;
206 | if args.lines:
207 | nlines = args.lines;
208 | output_lastlog(nlines);
209 | if not args.command:
210 | print_stats();
211 | listfiles();
212 |
213 | def check_root ():
214 | uid = os.getuid();
215 | if not uid == 0:
216 | sys.exit(bchar.FAIL + "Cannot access your Ventoy drive without root access.."+ bchar.ENDC +"\nTry again with"+ bchar.BOLD +" sudo " + " ".join(sys.argv) + bchar.ENDC);
217 | else:
218 | return 0;
219 |
220 | def print_stats ():
221 |
222 | if ventoy.ventoy_device:
223 | if ventoy.temp_dir:
224 | print(bchar.UNDERLINE + "Ventoy usage statistics:" + bchar.ENDC);
225 | print(bchar.BOLD + bchar.HEADER + "Total: " + bchar.ENDC + str(hr_filesize(shutil.disk_usage(ventoy.temp_dir).total)));
226 | print(bchar.BOLD + bchar.HEADER + "Used: " + bchar.ENDC + str(hr_filesize(shutil.disk_usage(ventoy.temp_dir).used)));
227 | print(bchar.BOLD + bchar.HEADER + "Free: " + bchar.ENDC + str(hr_filesize(shutil.disk_usage(ventoy.temp_dir).free)));
228 |
229 | def load_wrapper (msg, func):
230 | process = threading.Thread(name="load_wrapper_process", target=func);
231 | process.start();
232 | while process.is_alive():
233 | load(msg);
234 |
235 | def grequest (url):
236 | rval = requests.get(url).text;
237 | rqueue.put(rval);
238 |
239 | def install(umount=True, short_name=""):
240 |
241 | json_iterator = 0;
242 | available_os = [];
243 |
244 | if short_name == "":
245 | for os in json_data:
246 | print(bchar.BOLD + "[ID: "+ str(json_iterator) +"]"+bchar.ENDC+": " + bchar.OKGREEN + str(json_data[str(os)]["name"]) + bchar.ENDC)
247 | available_os.append(os);
248 | json_iterator += 1;
249 |
250 | select_install_os = input("Which OS do you want to install? [0-"+str(json_iterator-1)+"]: ");
251 | else:
252 | for os in json_data:
253 | available_os.append(os);
254 | try:
255 | select_install_os = str(available_os.index(str(short_name)));
256 | except ValueError:
257 | print(bchar.FAIL + "Error: Operating System " + bchar.BOLD + str(short_name) + bchar.ENDC + bchar.FAIL + " could not be found in your configuration file.\n"+bchar.HEADER+"Available are following:" + bchar.ENDC);
258 | print(", ".join([os for os in json_data]));
259 | sys.exit();
260 |
261 | try:
262 | try:
263 | url = json_data[available_os[int(select_install_os)]]["url"];
264 |
265 | #request = requests.get(url).text;
266 | global rqueue;
267 | rqueue = queue.Queue()
268 | request_process = threading.Thread(name="request_process", target=grequest, args=[str(url)]);
269 | request_process.start();
270 | while request_process.is_alive():
271 | load("Searching for installable versions");
272 |
273 | request_result = rqueue.get();
274 |
275 | sos_html_soup = BeautifulSoup(request_result, "html.parser");
276 | sos_iso_images = [ node.get("href") for node in sos_html_soup.find_all("a") if node.get("href").endswith(".iso")];
277 | sos_iso_images = list(dict.fromkeys(sos_iso_images))
278 | os_iterator = 0;
279 | print("\n");
280 | for image in sos_iso_images:
281 | image = image.split("/")[-1];
282 | print(bchar.BOLD + "[ID: "+ str(os_iterator) +"]"+bchar.ENDC+": " + bchar.OKGREEN + image + bchar.ENDC);
283 | os_iterator += 1;
284 |
285 | select_os_type = input("Which OS type do you want to install? [0-"+str(os_iterator-1)+"]: ");
286 | if select_os_type == "":
287 | if len(sos_iso_images) == 1:
288 | select_os_type = "0";
289 |
290 | url_regex = r"^http[s]:\/\/.*\/";
291 | download_match = re.search(url_regex, sos_iso_images[int(select_os_type)]);
292 | if download_match:
293 | url = re.findall(url_regex, sos_iso_images[int(select_os_type)]);
294 | url = url[0];
295 | sos_iso_images[int(select_os_type)] = str(sos_iso_images[int(select_os_type)]).split("/")[-1];
296 | try:
297 | #download_url = url + sos_iso_images[int(select_os_type)];
298 | download_file(ventoy.temp_dir, sos_iso_images[int(select_os_type)], url);
299 | print(bchar.OKGREEN + "Done. You can now use "+str(sos_iso_images[int(select_os_type)])+"..." + bchar.ENDC);
300 | if umount:
301 | umount_process = threading.Thread(name="umount_process", target=ventoy.umountVentoyDevice);
302 | umount_process.start();
303 | while umount_process.is_alive():
304 | load("Unmounting " + str(ventoy.ventoy_device) + "... I may still have to sync.");
305 | print("\n" + bchar.WARNING + "Ventoy device "+str(ventoy.ventoy_device)+ " umounted!" + bchar.ENDC);
306 | except IndexError or ValueError:
307 | sys.exit(bchar.FAIL + "Error: Select a valid operating system!" + bchar.ENDC);
308 | except KeyError or ValueError:
309 | sys.exit(bchar.FAIL + "Error: Interesting... Your JSON file looks broken. Cannot find parameter 'url' for os " + str(available_os[int(select_install_os)]) + bchar.ENDC);
310 | except IndexError or ValueError:
311 | sys.exit(bchar.FAIL + "Error: Select a valid operating system!" + bchar.ENDC);
312 |
313 | def check_update (silent=False):
314 |
315 | _version_search_regex = r"([0-9]{1,4}\.[0-9]{1,3}\.iso)|(([0-9]{1,4}\.[0-9]{1,4})\.[0-9]{1,4})|([0-9]{1,4}\.)";
316 |
317 | iso_files = ventoy.getISOFiles();
318 |
319 | if len(iso_files) < 1:
320 | if not silent:
321 | sys.exit(bchar.FAIL + "Error: Could not find any ISO images on your Ventoy device!" + bchar.ENDC);
322 |
323 | check_for_updates = {};
324 | sum_iso_info = {};
325 | shortlist_update = {};
326 |
327 | for os in json_data:
328 | url = json_data[str(os)]["url"];
329 | request_result = requests.get(url).text;
330 |
331 | html_soup = BeautifulSoup(request_result, "html.parser");
332 | fiso_files = [ node.get("href") for node in html_soup.find_all("a") if node.get("href").endswith(".iso")];
333 | for fil in fiso_files:
334 | fil = fil.split("/")[-1];
335 | sum_iso_info.update({str(fil): str(url)});
336 |
337 | if not len(sum_iso_info) > 1:
338 | if not silent:
339 | sys.exit(bchar.FAIL + "Error: Could not retrieve any iso file from your config file!" + bchar.ENDC);
340 |
341 | for iso in iso_files:
342 | iso = iso.split("/")[-1];
343 | for siso, downloadurl in sum_iso_info.items():
344 | similarity = SequenceMatcher(None, siso, iso).ratio();
345 | if similarity == 1.0:
346 | if verbose_mode:
347 | if not silent:
348 | print( bchar.WARNING + "Verbose: " + bchar.OKGREEN + str(iso) + " is already the newest version!" + bchar.ENDC);
349 | break;
350 | if similarity > 0.89:
351 | shortlist_update.update({str(iso): [str(downloadurl), str(siso)]});
352 | if verbose_mode:
353 | similarity = round(similarity*100, 1);
354 | if not silent:
355 | print(bchar.WARNING + "Verbose: Matching "+ iso +" and " + siso + " with "+bchar.BOLD+ str(similarity)+"%"+bchar.ENDC+bchar.WARNING+" similarity." + bchar.ENDC);
356 |
357 | for iso in shortlist_update:
358 |
359 | matched_similarity = shortlist_update[str(iso)][1];
360 | matched_url = shortlist_update[str(iso)][0];
361 |
362 | fregex = re.search(_version_search_regex, iso);
363 | sregex = re.search(_version_search_regex, matched_similarity);
364 |
365 | if fregex and sregex:
366 | frev = iso[:fregex.start()] + iso[fregex.end():];
367 | srev = matched_similarity[:sregex.start()] + matched_similarity[sregex.end():];
368 |
369 | similarity = SequenceMatcher(None, frev, srev).ratio();
370 | if similarity == 1.0:
371 | check_for_updates.update({str(iso): [shortlist_update[str(iso)][0], str(matched_similarity)]});
372 |
373 | if len(check_for_updates.items()) < 1:
374 | if not silent:
375 | if not verbose_mode:
376 | sys.exit(bchar.FAIL + "\nInfo: It seems that no ISO file on your Ventoy device is upgradable. You will get more information with " + bchar.BOLD + bchar.OKCYAN + "--verbose" + bchar.ENDC);
377 | else:
378 | sys.exit(bchar.FAIL + "\nInfo: It seems that no ISO file on your Ventoy device is upgradable." + bchar.ENDC);
379 |
380 | for i, x in check_for_updates.items():
381 | if not silent:
382 | print(bchar.HEADER + u"\u2192 " + str(i) + bchar.ENDC + bchar.OKGREEN + " seems to be upgradable!" + bchar.ENDC);
383 |
384 | if not silent:
385 | print(bchar.OKGREEN + "Type " + bchar.OKCYAN + bchar.BOLD + "sudo " + sys.argv[0].split("/")[-1] + " update " + bchar.ENDC + bchar.OKGREEN + "to update your ISO images."+ bchar.ENDC);
386 |
387 | return check_for_updates;
388 |
389 | def remove_iso ():
390 | verbose_mode = True;
391 | listfiles();
392 | print("\nType 'ALL' to delete all ISO images.");
393 | which_iso = input("\nWhich ISO file do you want to remove? ");
394 | ventoy_files = ventoy.getISOFiles();
395 | if which_iso == "ALL":
396 | for iso in ventoy_files:
397 | ventoy.deleteISO(str(iso));
398 | print("Deleted ISO file "+ str(iso) +"!");
399 |
400 | return;
401 |
402 | try:
403 | returnval = ventoy.deleteISO(str(ventoy_files[int(which_iso)]));
404 |
405 | except ValueError:
406 | returnval = ventoy.deleteISO(str(which_iso));
407 |
408 | if returnval:
409 | print("Deleted ISO image.");
410 | else:
411 | print("Could not delete ISO image.");
412 |
413 | def checkurl (explurl=False, url=None):
414 |
415 | checkurl = url;
416 | if not explurl:
417 | print("\nPlease type in the URL you want to check.");
418 | checkurl = input("URL: ");
419 | global rqueue;
420 | rqueue = queue.Queue()
421 | request_process = threading.Thread(name="request_process", target=grequest, args=[str(checkurl)]);
422 | request_process.start();
423 | while request_process.is_alive():
424 | load("Searching for installable versions");
425 |
426 | request_result = rqueue.get();
427 |
428 | sos_html_soup = BeautifulSoup(request_result, "html.parser");
429 | sos_iso_images = [ node.get("href") for node in sos_html_soup.find_all("a") if node.get("href").endswith(".iso")];
430 | os_iterator = 0;
431 | print("\n");
432 | for image in sos_iso_images:
433 | image = image.split("/")[-1];
434 | print(bchar.BOLD + "[ID: "+ str(os_iterator) +"]"+bchar.ENDC+": " + bchar.OKGREEN + image + bchar.ENDC);
435 | os_iterator += 1;
436 |
437 | if len(sos_iso_images) >= 1:
438 | print("\nI found "+ str(len(sos_iso_images)) +" files. You can add the URL by using following command:");
439 | print(bchar.HEADER + sys.argv[0] + " add-url --url " + checkurl + bchar.ENDC);
440 | else:
441 | print( bchar.WARNING + "Warning: I did not found any ISO images on " + bchar.HEADER + checkurl + bchar.ENDC + bchar.WARNING + ". Please try again with another URL." + bchar.ENDC);
442 |
443 | def listfiles ():
444 |
445 | files_found = ventoy.getISOFiles();
446 |
447 | if files_found:
448 | print(bchar.UNDERLINE + "I found following ISO images on your Ventoy device:\n" + bchar.ENDC);
449 | for iso in files_found:
450 | size = os.stat(str(iso)).st_size;
451 | size = hr_filesize(size);
452 | sizel = "";
453 | if verbose_mode:
454 | sizel = bchar.HEADER + " ("+ str(size) +") " + bchar.ENDC;
455 | print(bchar.OKGREEN + u"\u2192 " + iso + bchar.ENDC + ""+sizel+" "+bchar.UNDERLINE+"["+str(files_found.index(iso))+"]" + bchar.ENDC);
456 |
457 | if verbose_mode:
458 | print("\nYou can use the index numbers to run an action (e.g. update or remove)");
459 | else:
460 | print(bchar.WARNING + bchar.UNDERLINE + "Warning:"+bchar.ENDC + bchar.WARNING + "\nNo ISO images on your Ventoy device ("+ventoy.ventoy_device+") found. Maybe its not a valid Ventoy device?" + bchar.ENDC);
461 | print(bchar.HEADER + "Use following command to install ISO images: " + bchar.ENDC + bchar.BOLD + "\nsudo ventoyu install" +bchar.ENDC);
462 |
463 | def addurl (url, sname, lname):
464 |
465 | if isinstance(url, str):
466 | addurl = url;
467 | else:
468 | print("Please type in the URL you want to add to the JSON config file. You may want to check your URL with option check-url.");
469 | addurl = input("URL: ");
470 |
471 | if isinstance(sname, str):
472 | short_name = sname;
473 | else:
474 | print("Give your entry a short name like 'debian' or 'ubuntu'");
475 | short_name = input("Short name: ");
476 |
477 | if isinstance(lname, str):
478 | long_name = lname;
479 | else:
480 | print("Give your entry a long name like 'Debian Buster' or 'Manjaro KDE'");
481 | long_name = input("Long name: ");
482 |
483 | new_url_config = {
484 | str(short_name): {
485 | "name": str(long_name),
486 | "url": str(addurl)
487 | }
488 | };
489 |
490 | json_data.update(new_url_config);
491 | with open(etcdir + "/url_config.json", "w") as jsonconfig:
492 | json.dump(json_data, jsonconfig, indent=4);
493 |
494 | def config (action, force=False, subaction=""):
495 |
496 | if action == "modify":
497 | options = ventoy.getPossiblePluginOptions(subaction);
498 |
499 | print(bchar.HEADER + "You need to provide following values: " + bchar.UNDERLINE + str(options) + bchar.ENDC);
500 | print(bchar.OKBLUE + "If you do not want to set a given option, just leave it empty!" + bchar.ENDC);
501 | newopts = {};
502 |
503 | for option in options:
504 | optval = input(str(option) + ": ");
505 | newopts.update({str(option): str(optval)});
506 |
507 | rc = ventoy.configureVentoyPlugin(subaction, newopts);
508 | if rc == False:
509 | print(bchar.FAIL + "Error: See the ventoyl log information for details. ("+bchar.OKCYAN+"sudo ventoyu lastlog"+bchar.FAIL+")" + bchar.ENDC);
510 | if action == "install":
511 | print(bchar.HEADER + "Download latest Ventoy tarball to temp directory.. Trying to open the installer..." + bchar.ENDC);
512 | ventoy.installLatestVentoy(False, force);
513 | if action == "opengui":
514 | print(bchar.HEADER + "Download latest Ventoy tarball to temp directory.. Trying to open the GUI installer..." + bchar.ENDC);
515 | ventoy.installLatestVentoy(True);
516 |
517 | def update_iso ():
518 |
519 | upgradable = check_update(True);
520 |
521 | if isinstance(upgradable, dict):
522 | if verbose_mode:
523 | print(bchar.WARNING + "Verbose: Found "+str(len(upgradable))+" upgradable images. Trying to update them..." + bchar.ENDC);
524 |
525 | for iso, isoinfo in upgradable.items():
526 | print(bchar.HEADER + "Updating "+ bchar.BOLD + bchar.OKBLUE + str(iso) + bchar.ENDC + bchar.HEADER + " with " + bchar.BOLD + bchar.OKBLUE + str(isoinfo[1]) + bchar.ENDC + bchar.HEADER + "..." + bchar.ENDC);
527 |
528 | if not ventoy.isVentoyMounted():
529 | ventoy.mountVentoyDevice();
530 |
531 | try:
532 | download_file(ventoy.temp_dir, isoinfo[1], isoinfo[0]);
533 | except:
534 | print(bchar.FAIL + "Error: Could not update " + bchar.BOLD + str(iso) + bchar.ENDC + bchar.FAIL + "!" + bchar.ENDC);
535 | continue;
536 |
537 | old_iso_path = ventoy.temp_dir + "/" + iso;
538 | if os.path.isfile(old_iso_path):
539 | rc = ventoy.deleteISO(str(iso));
540 | if rc == True:
541 | if verbose_mode:
542 | print(bchar.WARNING + "Info: Deleted the old ISO image " + str(iso) + bchar.ENDC);
543 | print(bchar.OKGREEN + "Successfully updated " + bchar.BOLD + str(iso) + bchar.ENDC + bchar.OKGREEN + "!" + bchar.ENDC);
544 | else:
545 | print(bchar.FAIL + "Error: Could not delete the old ISO image " + str(iso) + bchar.ENDC);
546 |
547 |
548 | if len(upgradable) < 1:
549 | sys.exit(bchar.FAIL + "Error: Could not found any upgradable images. Try run " + bchar.OKCYAN + bchar.BOLD + "sudo " + sys.argv[0].split("/")[-1] + " --verbose check-update"+bchar.ENDC + bchar.FAIL + " manually." + bchar.ENDC);
550 |
551 | def output_lastlog (num_lines=10):
552 |
553 | log_file = ventoy.getVentoylLogFile();
554 | if log_file:
555 | rfile = open(log_file, "r");
556 | lines = rfile.readlines();
557 | for line in lines[-int(num_lines):]:
558 | print(line.rstrip("\n"));
559 |
560 | ventoyu_ascii = bchar.OKBLUE + """
561 | _ __ __ __ __
562 | | | / /__ ____ / /_____ __ __/ / / /
563 | | | / / _ \/ __ \/ __/ __ \/ / / / / / /
564 | | |/ / __/ / / / /_/ /_/ / /_/ / /_/ /
565 | |___/\___/_/ /_/\__/\____/\__, /\____/
566 | /____/
567 | """ + bchar.ENDC;
568 |
569 | if __name__ == "__main__":
570 | main();
571 |
--------------------------------------------------------------------------------
/imgs/ventoyu_install.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mawigh/ventoyu/fa8a631a3ff47a96b5ce57b107db44091b68f248/imgs/ventoyu_install.png
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | try:
2 | from setuptools import setup, find_packages;
3 | except ImportError:
4 | from distutils.core import setup;
5 |
6 | with open("README.md", "r", encoding="utf-8") as fh:
7 | long_description = fh.read();
8 |
9 | setup(
10 | name="ventoyu",
11 | version="0.4",
12 | author="Marcel-Brian Wilkowsky",
13 | author_email="marcel.wilkowsky@hotmail.de",
14 | description="Manage and update ISO files on Ventoy devices",
15 | long_description=long_description,
16 | long_description_content_type="text/markdown",
17 | url="https://github.com/mawigh/ventoy_updater",
18 | project_urls={
19 | "Bug Tracker": "https://github.com/mawigh/ventoy_updater/issues",
20 | },
21 | classifiers=[
22 | "Programming Language :: Python :: 3",
23 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
24 | "Operating System :: Unix",
25 | ],
26 | include_package_data=True,
27 | scripts=["bin/ventoyu"],
28 | install_requires=["beautifulsoup4", "requests"],
29 | package_dir={"": "src"},
30 | packages=find_packages(where="src"),
31 | )
32 |
--------------------------------------------------------------------------------
/src/ventoyl/__init__.py:
--------------------------------------------------------------------------------
1 | from .ventoyl import ventoyl;
2 |
--------------------------------------------------------------------------------
/src/ventoyl/etc/url_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "debianarm": {
3 | "name": "Debian (arm64)",
4 | "url": "https://cdimage.debian.org/debian-cd/current/arm64/iso-cd/"
5 | },
6 | "debianamd": {
7 | "name": "Debian (amd64)",
8 | "url": "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/"
9 | },
10 | "grml": {
11 | "name": "Grml",
12 | "url": "https://download.grml.org/"
13 | },
14 | "ubuntu1804": {
15 | "name": "Ubuntu (18.04)",
16 | "url": "https://ftp.halifax.rwth-aachen.de/ubuntu-releases/18.04/"
17 | },
18 | "ubuntu2004": {
19 | "name": "Ubuntu (20.04)",
20 | "url": "https://ftp.halifax.rwth-aachen.de/ubuntu-releases/20.04/"
21 | },
22 | "ubuntu2104": {
23 | "name": "Ubuntu (21.04)",
24 | "url": "https://ftp.halifax.rwth-aachen.de/ubuntu-releases/21.04/"
25 | },
26 | "manjaroxfce": {
27 | "name": "Manjaro (XFCE)",
28 | "url": "https://manjaro.org/downloads/official/xfce/"
29 | },
30 | "manjarokde": {
31 | "name": "Manjaro (KDE)",
32 | "url": "https://manjaro.org/downloads/official/kde/"
33 | },
34 | "finnix123": {
35 | "name": "Finnix 123",
36 | "url": "https://www.finnix.org/releases/123/"
37 | },
38 | "rockylinux8x64": {
39 | "name": "Rocky Linux 8 (x86_x64)",
40 | "url": "https://download.rockylinux.org/pub/rocky/8/isos/x86_64/"
41 | },
42 | "rockylinux8aarch": {
43 | "name": "Rocky Linux 8 (aarch64)",
44 | "url": "https://download.rockylinux.org/pub/rocky/8/isos/aarch64/"
45 | },
46 | "centos8": {
47 | "name": "CentOS 8",
48 | "url": "http://ftp.halifax.rwth-aachen.de/centos/8/isos/x86_64/"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/ventoyl/ventoyl.py:
--------------------------------------------------------------------------------
1 | # Author: Marcel-Brian Wilkowsky (mawigh)
2 |
3 | import json;
4 | import subprocess;
5 | import shutil;
6 | import re;
7 | import sys;
8 | import tempfile;
9 | import os;
10 | import logging;
11 | import ctypes;
12 | import ctypes.util;
13 | import requests;
14 | import tarfile;
15 |
16 | class debugp:
17 | COLOR = '\033[93m';
18 | ENDC = '\033[0m';
19 |
20 | class ventoyl:
21 |
22 | _Ventoy_git_releases = "https://api.github.com/repos/ventoy/Ventoy/releases/latest";
23 | _Ventoy_download_URL = "https://github.com/ventoy/Ventoy/releases/download/vRELEASE/ventoy-RELEASE-linux.tar.gz";
24 |
25 | def __init__ (self, ventoy_device=None, debug=False):
26 | self.device_mounted = False;
27 | self.iso_images = [];
28 | self.ventoy_device = ventoy_device;
29 | self.__easysearch = True;
30 | self.device_mounted = False;
31 | self.debug = debug;
32 |
33 | self.log_file = os.path.dirname(__file__) + "/ventoyl.log";
34 | if self.debug == True:
35 | loglevel = logging.DEBUG;
36 | else:
37 | loglevel = logging.INFO;
38 |
39 | logging.basicConfig(filename=self.log_file, format="[%(levelname)s - %(asctime)s] %(filename)s - %(funcName)s: %(message)s", encoding="utf-8", level=loglevel);
40 |
41 | if not isinstance(ventoy_device, str):
42 | rc = self.findVentoyDevice();
43 | if not rc == False:
44 | logging.info("Found the Ventoy device: " + str(rc["name"]));
45 |
46 | rc = self.checkVentoyMount();
47 | if rc == -1:
48 | if isinstance(ventoy_device, str):
49 | logging.error("The specified Ventoy device ("+str(ventoy_device)+") could not be found on your system.");
50 | raise OSError("The specified Ventoy device could not be found on your system.");
51 |
52 | try:
53 | self.mountVentoyDevice();
54 | except:
55 | if isinstance(self.ventoy_device, str):
56 | logging.debug("The specified device ("+str(self.ventoy_device)+") could not be found.");
57 |
58 | def findVentoyDevice (self):
59 |
60 | ls_blkd = shutil.which("lsblk");
61 | fdevice = "";
62 | if ls_blkd:
63 | lsblk_out = subprocess.check_output([ls_blkd, "-Jnpo", "label,name,fstype"]);
64 | json_parse = json.loads(lsblk_out);
65 | search_pattern = "[V,v]entoy";
66 | if isinstance(json_parse, dict):
67 | for key in json_parse["blockdevices"]:
68 | try:
69 |
70 | if key["children"]:
71 | for child in key["children"]:
72 | if re.search(search_pattern, str(child["label"])):
73 | fdevice = child;
74 | logging.debug("Found Ventoy device: " + str(child));
75 | except KeyError:
76 | pass;
77 | else:
78 | return False;
79 |
80 | if isinstance(fdevice, dict):
81 | self.ventoy_device = fdevice["name"];
82 | self.ventoy_devicefs = fdevice["fstype"];
83 | return fdevice;
84 | else:
85 | return False;
86 | else:
87 | return False;
88 |
89 | def checkVentoyMount (self):
90 | if not self.ventoy_device:
91 | self.findVentoyDevice();
92 |
93 | lsblk_out = subprocess.check_output(["lsblk","-Jnpo","name,mountpoint"]);
94 | json_parse = json.loads(lsblk_out);
95 | if isinstance(json_parse, dict):
96 | for key in json_parse["blockdevices"]:
97 | try:
98 | test = key["children"];
99 | except KeyError:
100 | return False;
101 | for child in key["children"]:
102 | if re.search(str(self.ventoy_device), str(child["name"])):
103 | if not child["mountpoint"] == None:
104 | self.device_mounted = True;
105 | self.temp_dir = child["mountpoint"];
106 | self.ventoy_config_dir = self.temp_dir + "/ventoy/";
107 | return child["mountpoint"];
108 | else:
109 | return False;
110 | else:
111 | return -1;
112 | else:
113 | return False;
114 |
115 | def createVentoyConfig (self):
116 |
117 | if not self.isVentoyMounted():
118 | rc = self.mountVentoyDevice();
119 | if rc == False:
120 | logging.error("Failed to mount your Ventoy drive!");
121 | return False;
122 |
123 | if not os.path.isdir(self.ventoy_config_dir):
124 | os.mkdir(self.ventoy_config_dir);
125 |
126 | if not os.path.isfile(self.ventoy_config_dir + "/ventoy.json"):
127 | new_file = open(self.ventoy_config_dir + "/ventoy.json", "w");
128 | new_file.write("{}");
129 | new_file.close();
130 |
131 | def getVentoyConfig (self):
132 |
133 | if not self.isVentoyMounted():
134 | self.mountVentoyDevice();
135 |
136 | if not os.path.isdir(self.ventoy_config_dir):
137 | logging.error("The Ventoy config directory does not exists. Maybe you want to create it with createVentoyConfig()?");
138 | return -1;
139 | if not os.path.isfile(self.ventoy_config_dir + "/ventoy.json"):
140 | logging.error("The Ventoy config file does not exists. Maybe you want to create it with createVentoyConfig()?");
141 | return -1;
142 |
143 | config_file = open(self.ventoy_config_dir + "/ventoy.json", "r");
144 | if config_file:
145 | try:
146 | jl = json.load(config_file);
147 | return jl;
148 | except json.decoder.JSONDecodeError:
149 | logging.error("Could not parse the Ventoy config file " + str(config_file));
150 | return False;
151 |
152 | def getVentoylLogFile (self):
153 |
154 | if self.log_file:
155 | return str(self.log_file);
156 | else:
157 | return False;
158 |
159 | def mountVentoyDevice (self):
160 | if not self.ventoy_device:
161 | return False;
162 |
163 | if not self.isVentoyMounted():
164 | self.temp_dir = tempfile.mkdtemp();
165 | libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True);
166 | libc.mount.argtypes = (ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_ulong, ctypes.c_char_p);
167 |
168 | return_val = libc.mount(str(self.ventoy_device).encode(), str(self.temp_dir).encode(), str(self.ventoy_devicefs).encode(), 0, "rw".encode());
169 | if return_val < 0:
170 | logging.error("Could not mount your Ventoy device "+str(self.ventoy_device)+"!");
171 | return False;
172 | else:
173 | logging.info("Successfully mounted Ventoy device "+str(self.ventoy_device)+" on " + str(self.temp_dir));
174 | self.device_mounted = True;
175 | return True;
176 |
177 | def umountVentoyDevice (self):
178 | if not self.isVentoyMounted():
179 | return False;
180 |
181 | umount_cmd = shutil.which("umount");
182 | rc = os.system(umount_cmd + " " + self.ventoy_device);
183 | if rc == 0:
184 | logging.info("Successfully umounted Ventoy device "+str(self.ventoy_device)+"!");
185 | return True;
186 | else:
187 | logging.error("Error trying to umount your Ventoy device "+str(self.ventoy_device)+"!");
188 | return False;
189 |
190 | def isVentoyMounted (self):
191 | if self.device_mounted:
192 | return True;
193 | else:
194 | return False;
195 |
196 | def getVentoyMountDir (self):
197 | if self.temp_dir:
198 | return self.temp_dir;
199 |
200 | def getISOFiles (self, scan=False):
201 |
202 | if not self.ventoy_device:
203 | return [];
204 | if not self.device_mounted:
205 | return [];
206 |
207 | if self.temp_dir:
208 | for f in os.listdir(self.temp_dir):
209 | if f.endswith(".iso"):
210 | self.iso_images.append(os.path.join(self.temp_dir, f));
211 |
212 | if len(self.iso_images) >= 1:
213 | return self.iso_images;
214 | else:
215 | return [];
216 |
217 | def deleteISO (self,iso_filename:str):
218 | if not self.ventoy_device:
219 | return False;
220 | if not self.device_mounted:
221 | return False;
222 |
223 | try:
224 | remove_iso = os.remove(iso_filename);
225 | logging.info("Successfully deleted ISO file "+str(iso_filename));
226 | return True;
227 | except FileNotFoundError:
228 | return False;
229 |
230 | def installLatestVentoy (self, gui=False, force=False):
231 |
232 | import platform;
233 |
234 | latest_release = requests.get(self._Ventoy_git_releases);
235 | latest_release = latest_release.json();
236 | latest_release = latest_release["tag_name"];
237 | latest_release = latest_release.replace("v", "");
238 | download_url = self._Ventoy_download_URL.replace("RELEASE", latest_release);
239 | file_name = download_url.split("/")[-1];
240 |
241 | download_dir = tempfile.mkdtemp();
242 | logging.debug("Download "+download_url+" to directory "+download_dir);
243 |
244 | download_file = requests.get(download_url);
245 | file_path = download_dir + "/" + file_name;
246 | with open(file_path, "wb") as tar_file:
247 | tar_file.write(download_file.content);
248 |
249 | if not os.path.isfile(file_path):
250 | logging.error("Cannot find file "+file_path+"!");
251 | sys.exit("Error: Cannot find "+ file_path +"!");
252 | else:
253 | logging.debug("Latest Ventoy release v"+ latest_release +" successfully downloaded");
254 |
255 | if file_name.endswith(".tar.gz"):
256 | etar = tarfile.open(file_path, "r:gz");
257 | etar.extractall(download_dir);
258 | etar.close();
259 | elif file_name.endswiith(".tar"):
260 | etar = tarfile.open(file_path, "r:");
261 | etar.extractall(download_dir);
262 | etar.close();
263 |
264 | if gui == True:
265 | gui_installer_path = download_dir + "/ventoy-" + latest_release + "/VentoyGUI." + platform.uname().machine;
266 | if os.path.isfile(gui_installer_path):
267 | os.system(gui_installer_path);
268 | else:
269 | logging.error("Cannot find the GUI installer ("+gui_installer_path+"). You may want to creat an issue on https://github.com/mawigh/ventoyu/issues");
270 | return False;
271 | else:
272 | shell_installer = download_dir + "/ventoy-" + latest_release + "/Ventoy2Disk.sh";
273 | if os.path.isfile(shell_installer):
274 | logging.debug("Launching the Ventoy Shell Installer "+shell_installer+"...");
275 |
276 | cmd = shell_installer + " -i " + self.ventoy_device;
277 | if force == True:
278 | cmd = shell_installer + " -I " + self.ventoy_device;
279 | os.system(cmd);
280 | else:
281 | logging.error("Cannot find the Ventoy shell installer " + shell_installer);
282 | return False;
283 |
284 | def getPossiblePlugins (self):
285 | ventoyPlugins = ["theme", "image_list"];
286 | return ventoyPlugins;
287 |
288 | def getPossiblePluginOptions (self, plugin:str):
289 |
290 | ventoyPlugins = {"theme": ["file", "gfxmode", "display_mode", "serial_param", "ventoy_left", "ventoy_top", "ventoy_color", "fonts"]};
291 | if not plugin in ventoyPlugins:
292 | return False;
293 |
294 | return ventoyPlugins[plugin];
295 |
296 |
297 | def configureVentoyPlugin (self, plugintype=None, pluginoptions=False):
298 |
299 | if not self.isVentoyMounted():
300 | rc = self.mountVentoyDevice();
301 | if rc == False:
302 | logging.error("Could not mount Ventoy drive! Maybe you should plug it in?");
303 | return False;
304 |
305 | vconfig = self.getVentoyConfig();
306 | if vconfig == -1:
307 | self.createVentoyConfig();
308 | vconfig = self.getVentoyConfig();
309 | elif vconfig == False:
310 | return False;
311 | if not plugintype in self.getPossiblePlugins():
312 | return False;
313 |
314 | new_plugin_config = {
315 | str(plugintype): {
316 | }
317 | };
318 |
319 | for option, value in pluginoptions.items():
320 | new_plugin_config[str(plugintype)].update({str(option): str(value)});
321 |
322 | vconfig.update(new_plugin_config);
323 | with open(str(self.ventoy_config_dir) + "/ventoy.json", "w") as ventoyconfig:
324 | json.dump(vconfig, ventoyconfig, indent=4);
325 |
326 | print(vconfig);
327 |
328 |
329 |
--------------------------------------------------------------------------------