Zapisywanie dużych plików z użyciem kodów QR - część 2, implementacja
Witajcie.
W poprzedniej części opisałem (zaproponowałem) sposób zapisu pliku w formie tekstów równej długości, które można zapisać np. na wielu kodach QR. Można go przeczytać tutaj:
Napisałem w języku Python implementację mojego pomysłu. Podzieliłem ją na osobną klasę kodera i dekodera:
import os
import base64
import hashlib
class FileToStringsCoder:
def __init__(self, file_path: str, max_string_lenght: int):
if not os.path.exists(file_path):
raise Exception("Error: the file does not exist.")
self.max_string_lenght = max_string_lenght
self.file_name = os.path.basename(file_path)
try:
self.extension = self.file_name.split(".")[-1]
except:
print("Failed to get file's extension, assuming .bin")
self.extension = "bin"
total_min_lenght = 40 + len(self.extension) + 1 + 4 + 1 + 4 + 1 + 2-2
#SHA1 + extension + separator + 4 digit number + separator + 4 digit number + separator + BASE64 or comment
if max_string_lenght <= total_min_lenght:
raise ValueError("Error: max_string_lenght is too small. It must be at least "+str(total_min_lenght+1))
base64_split_size = max_string_lenght - total_min_lenght
base64_original = self.file_to_base64(file_path)
self.base64_hash = hashlib.sha1(base64_original.encode()).hexdigest()
#print(base64_original)
#print(base64_split_size)
self.base64_split = self.split_string_into_fragments(base64_original, base64_split_size)
del base64_original
self.generated_amount = len(self.base64_split)
print(str(self.generated_amount)+" strings will be created.")
if len(self.base64_split)>=14776334:
raise Exception("Error, only up to 14776334 strings can be created.")
#print(self.base64_split)
def generate_strings(self, description = None, display_strings = False):
results = []
n = 0
for i in self.base64_split:
text = self.base64_hash + self.extension + "$" + str(self.int_to_qr_base(n)) + "$" + str(self.int_to_qr_base(self.generated_amount)) + "$"
if n==0 and description!=None and len(description)>=2:
text = text + "@" + description + "@" + i
else:
text = text + i
n+=1
if display_strings:
print(text)
print(" ")
results.append(text)
print("Finished generating " + str(len(results)) + " strings.")
return results
def split_string_into_fragments(self, input_string, fragment_length):
return [input_string[i:i+fragment_length] for i in range(0, len(input_string), fragment_length)]
def file_to_base64(self, file_path):
with open(file_path, "rb") as file:
file_bytes = file.read()
base64_bytes = base64.b64encode(file_bytes)
return base64_bytes.decode("utf-8")
def int_to_qr_base(self, n):
qr_base_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuwxyz"
qr_base_len = len(qr_base_chars)
result = ""
while n > 0:
n, remainder = divmod(n, qr_base_len)
result = qr_base_chars[remainder] + result
# Pad with zeros if necessary
while len(result) < 4:
result = qr_base_chars[0] + result
return result
class FileToStringsDecoder:
def __init__(self):
self.hash = None
self.extension = None
self.amount = None
self.base64_list = []
self.description = "[None]"
def add_decoded_string(self, string: str):
if (len(string) < 40+1+1+4+1+4+1+1):
print("Error: this string (" + string + ") is too short to be a valid one. Skipping")
else:
try:
readhash = string[0:40]
temp = string[40:]
string_table = temp.split("$")
del temp
#print(string_table)
if self.hash == None and self.extension == None and self.amount == None:
self.hash = readhash
self.extension = string_table[0]
self.amount = string_table[2]
self.base64_list = [None] * self.qr_base_to_int(self.amount)
print("SHA1: "+self.hash+"\nExtension: "+self.extension+"\nAmount: "+str(self.qr_base_to_int(self.amount)))
if readhash == self.hash and string_table[0] == self.extension and string_table[2] == self.amount:
base64_fragment = string_table[-1]
if base64_fragment[0]=="@": #extract description
temp = base64_fragment[1:].split("@")
self.description = temp[0]
print("Decoded description: " + self.description)
self.base64_list[self.qr_base_to_int(string_table[1])] = temp[1]
del temp
else:
self.base64_list[self.qr_base_to_int(string_table[1])] = base64_fragment
del base64_fragment
else:
raise ValueError("Error: this string comes from a different file.")
self.check_how_many_left()
except:
raise Exception("Error decoding this string.")
def qr_base_to_int(self, qr_base_str):
qr_base_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuwxyz"
qr_base_len = len(qr_base_chars)
result = 0
for char in qr_base_str:
result = result * qr_base_len + qr_base_chars.index(char)
return result
def check_how_many_left(self, print_list = False):
n = 0
for i in self.base64_list:
if i!=None:
n+=1
if print_list:
print(i)
print("Collected "+str(n)+" out of "+str(len(self.base64_list))+" strings.")
def get_list(self):
return self.base64_list
def assemble_file(self, result_file_path: str):
file_base64 = ""
for i in self.base64_list:
if i==None:
print("Error: not all codes collected. Aborting...")
return None
file_base64 = file_base64 + i
if self.hash != hashlib.sha1(file_base64.encode()).hexdigest():
raise Exception("Error: final SHA1 hash does not match the original one.")
else:
self.base64_to_file(file_base64, result_file_path+"."+self.extension)
print("Saved file to "+result_file_path+"."+self.extension)
def base64_to_file(self, base64_string, output_file_path):
base64_bytes = base64_string.encode("utf-8")
file_bytes = base64.b64decode(base64_bytes)
with open(output_file_path, "wb") as file:
file.write(file_bytes)
Zgodnie z moim pomysłem, najpierw generowana jest reprezentacja pliku w BASE64, następnie SHA1 tego BASE64, obliczania jest długość fragmentu BASE64 aby każdy wygenerowany string miał taką samą długość.
Ostatecznie powstaje wiele tekstów w formacie:
[SHA1][rozszerzenie pliku]$[numer tekstu]$[ilość tekstów]$[BASE64 (fragment)]
Użyłem znaku $ jako separator, oraz @ jako granice ewentualnego komentarza.
Dekoder zgodnie z moim pomysłem na podstawie pierwszego kodu/tekstu tworzy listę w rozmiarze na wszystkie teksty a następnie umieszcza w niej fragmenty BASE64 w odpowiedniej kolejności. Ponieważ każdy tekst zapisuje własny numer, mogą być one wczytywane w dowolnej kolejności. Sprawdzane są hash, rozszerzenie pliku oraz ilość wszystkich tekstów sprawdza, czy następne odczytane teksty zawierają ten sam plik. Na końcu, przed odtworzeniem pliku, sprawdzany jest hash aby potwierdzić że plik wynikowy będzie taki sam jak wejściowy.
Dodam jeszcze, że liczby zawsze są 4-cyfrowe i zapisywane w systemie liczbowym o podstawie 62, co umożliwia zapis kilku milionów liczb.
Przykładowy tekst wynikowy (jeden ze wszystkich) wygląda następująco:
47ee464c619b52642750bb5f81031e8f768e310cexe$0000$00Iz$TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAA
Program zadziałał zgodnie z moim pomysłem i nadaje się do dzielenia plików na łatwe do zdekodowania fragmenty równej długości. Skutecznie także zakodowałem plik w kodach QR.
40 kilobajtowy program zapisany w kodach QR, zajęło to 4 strony.
Dalsze ulepszenia:
Po napisaniu tej implementacji, doszedłem do wniosku, że mógłby być jeszcze lepiej zoptymalizowany:
- Zamiast $ można by jednak wykorzystywać znak spacji. Nie wprowadziłoby to różnicy (to tak samo jeden symbol jak $), a np. iteratory plikowe w C++ domyślnie wykorzystują znaki białe jako separator, co ułatwiałoby stworzenie implementacji w tym języku programowania.
- Zamiast liczby o podstawie 62 kodowanej osobną funkcją, liczbę można by także kodować w BASE64, co pozwoliłoby na zapis kilku milionów liczb więcej oraz wykorzystywało tę samą funkcję co kodowanie i dekodowanie pliku.
- Format tekstu mógłby zawierać elementy stałej długości i niezmienne połączone razem na początku:
[SHA1][ilość tekstów][rozszerzenie pliku] [numer tekstu] [BASE64 (fragment)]
Dzięki temu pierwsze 3 informacje mogłyby być odczytywane jako jeden blok tekstu co jest bardziej efektywne, oraz wykorzystywalibyśmy mniej symboli separatora, zostawiając więcej miejsca na dane.
Możliwe że wprowadzę te zmiany w 3 części artykułu i jego angielskiej wersji, jeżeli ją napiszę.
Dziękuję za przeczytanie tego posta. Do zobaczenia w kolejnych!
Komentarze
Prześlij komentarz