diff --git a/tools/otagui/.gitignore b/tools/otagui/.gitignore index 21f295a0c..b81e39b9a 100644 --- a/tools/otagui/.gitignore +++ b/tools/otagui/.gitignore @@ -25,3 +25,4 @@ pnpm-debug.log* *.njsproj *.sln *.sw? +*.db diff --git a/tools/otagui/.prettierrc.js b/tools/otagui/.prettierrc.js index a6b80c2e1..09167982e 100644 --- a/tools/otagui/.prettierrc.js +++ b/tools/otagui/.prettierrc.js @@ -1,4 +1,5 @@ module.exports = { singleQuote: true, - semi: false + semi: false, + useTabs: false } diff --git a/tools/otagui/ota_interface.py b/tools/otagui/ota_interface.py index 387ddddbf..f5b5fb036 100644 --- a/tools/otagui/ota_interface.py +++ b/tools/otagui/ota_interface.py @@ -49,14 +49,10 @@ class ProcessesManagement: def get_status(self): return [self.get_status_by_ID(id=id) for id in self.get_keys()] - def get_list(self, dir): - files = os.listdir(dir) - return files - def ota_generate(self, args, id=0): command = ['ota_from_target_files'] # Check essential configuration is properly set - if not os.path.isfile('target/' + args['target']): + if not os.path.isfile(args['target']): raise FileNotFoundError if not args['output']: raise SyntaxError @@ -65,14 +61,17 @@ class ProcessesManagement: command.append('-k') command.append( '../../../build/make/target/product/security/testkey') - if args['incremental']: - if not os.path.isfile('target/' + args['incremental']): + if args['isIncremental']: + if not os.path.isfile(args['incremental']): raise FileNotFoundError command.append('-i') - command.append('target/' + args['incremental']) - command.append('target/' + args['target']) + command.append(args['incremental']) + if args['isPartial']: + command.append('--partial') + command.append(args['partial']) + command.append(args['target']) command.append(args['output']) - + # Start a subprocess and collect the output stderr_pipes = pipes.Template() stdout_pipes = pipes.Template() ferr = stderr_pipes.open(os.path.join( diff --git a/tools/otagui/src/components/BaseCheckbox.vue b/tools/otagui/src/components/BaseCheckbox.vue index e9e0d24ce..21a697789 100644 --- a/tools/otagui/src/components/BaseCheckbox.vue +++ b/tools/otagui/src/components/BaseCheckbox.vue @@ -3,6 +3,7 @@ type="checkbox" :checked="modelValue" class="field" + v-bind="$attrs" @change="$emit('update:modelValue', $event.target.checked)" > diff --git a/tools/otagui/src/components/FileSelect.vue b/tools/otagui/src/components/FileSelect.vue new file mode 100644 index 000000000..42e39a9fa --- /dev/null +++ b/tools/otagui/src/components/FileSelect.vue @@ -0,0 +1,39 @@ + + + + + \ No newline at end of file diff --git a/tools/otagui/src/components/PartialCheckbox.vue b/tools/otagui/src/components/PartialCheckbox.vue new file mode 100644 index 000000000..426b261f8 --- /dev/null +++ b/tools/otagui/src/components/PartialCheckbox.vue @@ -0,0 +1,37 @@ + + + \ No newline at end of file diff --git a/tools/otagui/src/views/SimpleForm.vue b/tools/otagui/src/views/SimpleForm.vue index 6357a3521..6d678a561 100644 --- a/tools/otagui/src/views/SimpleForm.vue +++ b/tools/otagui/src/views/SimpleForm.vue @@ -3,16 +3,16 @@

- - +
  • +
    +
    Build File Name: {{ targetDetail.file_name }}
    + Uploaded time: {{ formDate(targetDetail.time) }} +
    + Build ID: {{ targetDetail.build_id }} +
    + Build Version: {{ targetDetail.build_version }} +
    + Build Flavor: {{ targetDetail.build_flavor }} +
    + +   + +
    +
  • + + diff --git a/tools/otagui/target_lib.py b/tools/otagui/target_lib.py new file mode 100644 index 000000000..76d7a8208 --- /dev/null +++ b/tools/otagui/target_lib.py @@ -0,0 +1,158 @@ +from dataclasses import dataclass, asdict, field +import sqlite3 +import time +import logging +import os +import zipfile +import re +import json + + +@dataclass +class BuildInfo: + """ + A class for Android build information. + """ + file_name: str + path: str + time: int + build_id: str = '' + build_version: str = '' + build_flavor: str = '' + partitions: list[str] = field(default_factory=list) + + def analyse_buildprop(self): + """ + Analyse the build's version info and partitions included + Then write them into the build_info + """ + def extract_info(pattern, lines): + # Try to match a regex in a list of string + line = list(filter(pattern.search, lines))[0] + if line: + return pattern.search(line).group(0) + else: + return '' + + build = zipfile.ZipFile(self.path) + try: + with build.open('SYSTEM/build.prop', 'r') as build_prop: + raw_info = build_prop.readlines() + pattern_id = re.compile(b'(?<=ro\.build\.id\=).+') + pattern_version = re.compile( + b'(?<=ro\.build\.version\.incremental\=).+') + pattern_flavor = re.compile(b'(?<=ro\.build\.flavor\=).+') + self.build_id = extract_info( + pattern_id, raw_info).decode('utf-8') + self.build_version = extract_info( + pattern_version, raw_info).decode('utf-8') + self.build_flavor = extract_info( + pattern_flavor, raw_info).decode('utf-8') + except KeyError: + pass + try: + with build.open('META/ab_partitions.txt', 'r') as partition_info: + raw_info = partition_info.readlines() + for line in raw_info: + self.partitions.append(line.decode('utf-8').rstrip()) + except KeyError: + pass + + def to_sql_form_dict(self): + sql_form_dict = asdict(self) + sql_form_dict['partitions'] = ','.join(sql_form_dict['partitions']) + return sql_form_dict + + def to_dict(self): + return asdict(self) + + +class TargetLib: + def __init__(self, path='ota_database.db'): + """ + Create a build table if not existing + """ + self.path = path + with sqlite3.connect(self.path) as connect: + cursor = connect.cursor() + cursor.execute(""" + CREATE TABLE if not exists Builds ( + FileName TEXT, + UploadTime INTEGER, + Path TEXT, + BuildID TEXT, + BuildVersion TEXT, + BuildFlavor TEXT, + Partitions TEXT + ) + """) + + def new_build(self, filename, path): + """ + Insert a new build into the database + Args: + filename: the name of the file + path: the relative path of the file + """ + build_info = BuildInfo(filename, path, int(time.time())) + build_info.analyse_buildprop() + with sqlite3.connect(self.path) as connect: + cursor = connect.cursor() + cursor.execute(""" + SELECT * FROM Builds WHERE FileName=:file_name and Path=:path + """, build_info.to_sql_form_dict()) + if cursor.fetchall(): + cursor.execute(""" + DELETE FROM Builds WHERE FileName=:file_name and Path=:path + """, build_info.to_sql_form_dict()) + cursor.execute(""" + INSERT INTO Builds (FileName, UploadTime, Path, BuildID, BuildVersion, BuildFlavor, Partitions) + VALUES (:file_name, :time, :path, :build_id, :build_version, :build_flavor, :partitions) + """, build_info.to_sql_form_dict()) + + def new_build_from_dir(self, path): + """ + Update the database using files under a directory + Args: + path: a directory + """ + if os.path.isdir(path): + builds_name = os.listdir(path) + for build_name in builds_name: + self.new_build(build_name, os.path.join(path, build_name)) + elif os.path.isfile(path): + self.new_build(os.path.split(path)[-1], path) + return self.get_builds() + + def sql_to_buildinfo(self, row): + build_info = BuildInfo(*row[:6], row[6].split(',')) + return build_info + + def get_builds(self): + """ + Get a list of builds in the database + Return: + A list of build_info, each of which is an object: + (FileName, UploadTime, Path, Build ID, Build Version, Build Flavor, Partitions) + """ + with sqlite3.connect(self.path) as connect: + cursor = connect.cursor() + cursor.execute(""" + SELECT FileName, Path, UploadTime, BuildID, BuildVersion, BuildFlavor, Partitions + FROM Builds""") + return list(map(self.sql_to_buildinfo, cursor.fetchall())) + + def get_builds_by_path(self, path): + """ + Get a build in the database by its path + Return: + A build_info, which is an object: + (FileName, UploadTime, Path, Build ID, Build Version, Build Flavor, Partitions) + """ + with sqlite3.connect(self.path) as connect: + cursor = connect.cursor() + cursor.execute(""" + SELECT FileName, Path, UploadTime, BuildID, BuildVersion, BuildFlavor, Partitions + WHERE Path==(?) + """, (path, )) + return self.sql_to_buildinfo(cursor.fetchone()) diff --git a/tools/otagui/web_server.py b/tools/otagui/web_server.py index 3ecf18f62..79c359903 100644 --- a/tools/otagui/web_server.py +++ b/tools/otagui/web_server.py @@ -9,12 +9,17 @@ API:: GET /check : check the status of all jobs GET /check/ : check the status of the job with GET /file : fetch the target file list + GET /file/ : Add build file(s) in , and return the target file list GET /download/ : download the ota package with POST /run/ : submit a job with , arguments set in a json uploaded together POST /file/ : upload a target file [TODO] POST /cancel/ : cancel a job with +TODO: + - Avoid unintentionally path leakage + - Avoid overwriting build when uploading build with same file name + Other GET request will be redirected to the static request under 'dist' directory """ @@ -22,12 +27,14 @@ from http.server import BaseHTTPRequestHandler, SimpleHTTPRequestHandler, HTTPSe from socketserver import ThreadingMixIn from threading import Lock from ota_interface import ProcessesManagement +from target_lib import TargetLib import logging import json import pipes import cgi import subprocess import os +import sys LOCAL_ADDRESS = '0.0.0.0' @@ -77,10 +84,14 @@ class RequestHandler(CORSSimpleHTTPHandler): ) return elif self.path.startswith('/file'): - file_list = jobs.get_list(self.path[6:]) + if self.path == '/file' or self.path == '/file/': + file_list = target_lib.get_builds() + else: + file_list = target_lib.new_build_from_dir(self.path[6:]) + builds_info = [build.to_dict() for build in file_list] self._set_response(type='application/json') self.wfile.write( - json.dumps(file_list).encode() + json.dumps(builds_info).encode() ) logging.info( "GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n", @@ -133,6 +144,7 @@ class RequestHandler(CORSSimpleHTTPHandler): file_length -= len(self.rfile.readline()) file_length -= len(self.rfile.readline()) output_file.write(self.rfile.read(file_length)) + target_lib.new_build(self.path[6:], file_name) self._set_response(code=201) self.wfile.write( "File received, saved into {}".format( @@ -164,8 +176,16 @@ def run_server(SeverClass=ThreadedHTTPServer, HandlerClass=RequestHandler, port= if __name__ == '__main__': from sys import argv print(argv) + if not os.path.isdir('target'): + os.mkdir('target', 755) + if not os.path.isdir('output'): + os.mkdir('output', 755) + target_lib = TargetLib() jobs = ProcessesManagement() - if len(argv) == 2: - run_server(port=int(argv[1])) - else: - run_server() + try: + if len(argv) == 2: + run_server(port=int(argv[1])) + else: + run_server() + except KeyboardInterrupt: + sys.exit(0)