Merge "Add support for uploading and downloading." am: 316ce8acca

Original change: https://android-review.googlesource.com/c/platform/development/+/1728650

Change-Id: Ie410316f1e99a045445a5fee6e19abc799e7c547
This commit is contained in:
Treehugger Robot
2021-06-08 19:51:37 +00:00
committed by Automerger Merge Worker
12 changed files with 414 additions and 166 deletions

View File

@@ -15,6 +15,13 @@ In this case we use `lunch 17` as an example (aosp-x86_64-cf), you can choose wh
Then, in this directory, please use `npm build` to install the dependencies. Then, in this directory, please use `npm build` to install the dependencies.
Create a `target` directory to store the target files and a `output` directory
to store the output files:
```
mkdir target
mkdir output
```
Finally, run the python http-server and vue.js server: Finally, run the python http-server and vue.js server:
``` ```
python3 web_server.py & python3 web_server.py &

View File

@@ -0,0 +1,86 @@
import subprocess
import os
import json
import pipes
from threading import Lock
import logging
class ProcessesManagement:
def __init__(self):
self.__container = {}
self.__lock = Lock()
def set(self, name, value):
with self.__lock:
self.__container[name] = value
def get(self, name):
with self.__lock:
return self.__container[name]
def get_keys(self):
with self.__lock:
return self.__container.keys()
def get_status_by_ID(self, id=0, details=False):
status = {}
if not id in self.get_keys():
return '{}'
else:
status['id'] = id
if self.get(id).poll() == None:
status['status'] = 'Running'
elif self.get(id).poll() == 0:
status['status'] = 'Finished'
status['path'] = os.path.join('output', str(id) + '.zip')
else:
status['status'] = 'Error'
try:
if details:
with open(os.path.join('output', 'stdout.' + str(id)), 'r') as fout:
status['stdout'] = fout.read()
with open(os.path.join('output', 'stderr.' + str(id)), 'r') as ferr:
status['stderr'] = ferr.read()
except FileNotFoundError:
status['stdout'] = 'NO STD OUTPUT IS FOUND'
status['stderr'] = 'NO STD OUTPUT IS FOUND'
return status
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']):
raise FileNotFoundError
if not args['output']:
raise SyntaxError
if args['verbose']:
command.append('-v')
command.append('-k')
command.append(
'../../../build/make/target/product/security/testkey')
if args['incremental']:
if not os.path.isfile('target/' + args['incremental']):
raise FileNotFoundError
command.append('-i')
command.append('target/' + args['incremental'])
command.append('target/' + args['target'])
command.append(args['output'])
stderr_pipes = pipes.Template()
stdout_pipes = pipes.Template()
ferr = stderr_pipes.open(os.path.join(
'output', 'stderr.'+str(id)), 'w')
fout = stdout_pipes.open(os.path.join(
'output', 'stdout.'+str(id)), 'w')
self.set(id, subprocess.Popen(
command, stderr=ferr, stdout=fout))
logging.info(
'Starting generating OTA package with id {}: \n {}'
.format(id, command))

View File

@@ -12,6 +12,7 @@
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",
"vue": "^3.0.0-0", "vue": "^3.0.0-0",
"vue-router": "^4.0.0-0", "vue-router": "^4.0.0-0",
"vue-uuid": "^2.0.2",
"vuex": "^4.0.0-0" "vuex": "^4.0.0-0"
}, },
"devDependencies": { "devDependencies": {
@@ -1479,6 +1480,11 @@
"source-map": "^0.6.1" "source-map": "^0.6.1"
} }
}, },
"node_modules/@types/uuid": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ=="
},
"node_modules/@types/webpack": { "node_modules/@types/webpack": {
"version": "4.41.21", "version": "4.41.21",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.21.tgz", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.21.tgz",
@@ -13339,6 +13345,23 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true "dev": true
}, },
"node_modules/vue-uuid": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/vue-uuid/-/vue-uuid-2.0.2.tgz",
"integrity": "sha512-PRf1CHg3uKi77bVRyAuW2u/T2PO9LxMr7cw9t9rNdpZTkNDyw1Fx6eJVL+8JOtM9VxxPkoZ/rwhXJ5l+X5AYzQ==",
"dependencies": {
"@types/uuid": "^8.0.0",
"uuid": "^8.1.0"
}
},
"node_modules/vue-uuid/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vuex": { "node_modules/vuex": {
"version": "4.0.0-beta.4", "version": "4.0.0-beta.4",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.0-beta.4.tgz", "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.0-beta.4.tgz",
@@ -15792,6 +15815,11 @@
"source-map": "^0.6.1" "source-map": "^0.6.1"
} }
}, },
"@types/uuid": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ=="
},
"@types/webpack": { "@types/webpack": {
"version": "4.41.21", "version": "4.41.21",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.21.tgz", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.21.tgz",
@@ -25710,6 +25738,22 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true "dev": true
}, },
"vue-uuid": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/vue-uuid/-/vue-uuid-2.0.2.tgz",
"integrity": "sha512-PRf1CHg3uKi77bVRyAuW2u/T2PO9LxMr7cw9t9rNdpZTkNDyw1Fx6eJVL+8JOtM9VxxPkoZ/rwhXJ5l+X5AYzQ==",
"requires": {
"@types/uuid": "^8.0.0",
"uuid": "^8.1.0"
},
"dependencies": {
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
}
}
},
"vuex": { "vuex": {
"version": "4.0.0-beta.4", "version": "4.0.0-beta.4",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.0-beta.4.tgz", "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.0-beta.4.tgz",

View File

@@ -12,6 +12,7 @@
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",
"vue": "^3.0.0-0", "vue": "^3.0.0-0",
"vue-router": "^4.0.0-0", "vue-router": "^4.0.0-0",
"vue-uuid": "^2.0.2",
"vuex": "^4.0.0-0" "vuex": "^4.0.0-0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,7 +1,7 @@
<template> <template>
<label class="file-select"> <label class="file-select">
<div class="select-button"> <div class="select-button">
<span v-if="value">Selected File: {{ value.name }}</span> <span v-if="label">{{ label }}</span>
<span v-else>Select File</span> <span v-else>Select File</span>
</div> </div>
<input <input
@@ -14,12 +14,14 @@
<script> <script>
export default { export default {
props: { props: {
value: File label: {
type: String,
default: ''
}
}, },
methods: { methods: {
handleFileChange(e) { handleFileChange(e) {
this.$emit('input', e.target.files[0]) this.$emit('file-select', e.target.files)
} }
} }
} }

View File

@@ -1,5 +1,6 @@
<template> <template>
<label> {{ label }} </label> <label> {{ label }} </label>
<br>
<input <input
v-bind="$attrs" v-bind="$attrs"
:placeholder="label" :placeholder="label"

View File

@@ -1,8 +1,12 @@
<template> <template>
<div class="job-display"> <router-link
<span>Status of Job.{{ job.id }}</span> :to="{ name: 'JobDetails', params: {id: job.id} }"
<h4>{{ job.status }}</h4> >
</div> <div class="job-display">
<span>Status of Job.{{ job.id }}</span>
<h4>{{ job.status }}</h4>
</div>
</router-link>
</template> </template>
<script> <script>

View File

@@ -0,0 +1,53 @@
<template>
<div>
<BaseFile
label="Upload a target file"
@file-select="UploadHandler"
/>
<br>
<progress
v-if="uploadPercentage > 0"
max="100"
:value.prop="uploadPercentage"
/>
</div>
</template>
<script>
import BaseFile from '@/components/BaseFile.vue'
import ApiService from '../services/ApiService.js'
export default {
components: {
BaseFile,
},
data() {
return {
files: '',
uploadPercentage: 0,
}
},
methods: {
async UploadHandler(files) {
this.file = files[0]
console.log(this.file.name)
try {
let response = await ApiService.uploadTarget(this.file, (event) => {
this.uploadPercentage = Math.round((100 * event.loaded) / event.total)
})
console.log(response)
this.$emit('file-uploaded')
} catch (err) {
console.log(err)
}
},
},
}
</script>
<style scoped>
progress {
width: 100%;
height: 40px;
}
</style>

View File

@@ -1,6 +1,6 @@
import axios from 'axios' import axios from 'axios'
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: 'http://localhost:8000', baseURL: 'http://localhost:8000',
withCredentials: false, withCredentials: false,
headers: { headers: {
@@ -13,11 +13,25 @@ export default {
getJobs() { getJobs() {
return apiClient.get("/check") return apiClient.get("/check")
}, },
getJobById(id) {
return apiClient.get("/check/" + id)
},
getFileList(path) {
return apiClient.get("/file" + path)
},
uploadTarget(file, onUploadProgress) {
let formData = new FormData()
formData.append('file', file)
return apiClient.post("/file/" + file.name,
formData,
{
onUploadProgress
})
},
async postInput(input, id) { async postInput(input, id) {
try { try {
const response = await apiClient.post( const response = await apiClient.post(
'/run/' + id, input) '/run/' + id, input)
console.log('Response:', response)
return response return response
} catch (err) { } catch (err) {
console.log('err:', err) console.log('err:', err)

View File

@@ -1,23 +1,23 @@
<template> <template>
<div v-if="job"> <div v-if="job">
<h3> Job. {{ job.id }} {{ job.status }}</h3> <h3>Job. {{ job.id }} {{ job.status }}</h3>
<div> <div>
<h4> STDERR </h4> <h4>STDERR</h4>
<div class="stderr"> <div class="stderr">
{{ job.stderr }} {{ job.stderr }}
<p ref="stderr_bottom" />
</div> </div>
<h4> STDOUT </h4> <h4>STDOUT</h4>
<div class="stdout"> <div class="stdout">
{{ job.stdout }} {{ job.stdout }}
<p ref="stdout_bottom" />
</div> </div>
</div> </div>
<br> <br>
<a <a
v-if="job.status=='Finished'" v-if="job.status == 'Finished'"
:href="download" :href="download"
> > Download </a>
Download
</a>
</div> </div>
</template> </template>
@@ -32,33 +32,35 @@ export default {
}, },
computed: { computed: {
download() { download() {
return "http://localhost:8000/download/" + this.job.path return 'http://localhost:8000/download/' + this.job.path
} },
}, },
created() { created() {
this.updateStatus() this.updateStatus()
}, },
methods: { methods: {
async updateStatus() { async updateStatus() {
// fetch job (by id) and set local job data // fetch job (by id) and set local job data
try { try {
let response = await ApiService.getJobById(this.id) let response = await ApiService.getJobById(this.id)
this.job = response.data this.job = response.data
await this.$refs.stdout_bottom.scrollIntoView({ behavior: 'smooth' })
await this.$refs.stderr_bottom.scrollIntoView({ behavior: 'smooth' })
} catch (err) {
console.log(err)
} }
catch (err) { if (this.job.status == 'Running') {
console.log(error)
}
if (this.job.status=='Running') {
setTimeout(this.updateStatus, 1000) setTimeout(this.updateStatus, 1000)
} }
} },
} },
} }
</script> </script>
<style scoped> <style scoped>
.stderr, .stdout { .stderr,
overflow: scroll; .stdout {
height: 200px; overflow: scroll;
height: 200px;
} }
</style> </style>

View File

@@ -1,57 +1,63 @@
<template> <template>
<div> <div>
<form @submit.prevent="sendForm"> <form @submit.prevent="sendForm">
<BaseInput <UploadFile @file-uploaded="fetchTargetList" />
<br>
<BaseSelect
v-if="input.incrementalStatus"
v-model="input.incremental" v-model="input.incremental"
:disabled="!input.incrementalStatus" label="Select the source file"
:label="'Source Package Path'" :options="targetList"
type="text"
/> />
<BaseSelect
<BaseInput
v-model="input.target" v-model="input.target"
label="Target File path" label="Select the target file"
type="text" :options="targetList"
/> />
<button
<BaseCheckbox type="button"
v-model="input.verbose" @click="fetchTargetList"
:label="'Verbose'" >
/> Update File List
</button>
<div>
<BaseCheckbox <BaseCheckbox
v-model="input.incrementalStatus" v-model="input.verbose"
:label="'Incremental'" :label="'Verbose'"
/> />
&emsp;
<BaseCheckbox
v-model="input.incrementalStatus"
:label="'Incremental'"
/>
</div>
<br>
<BaseInput <BaseInput
v-model="input.output" v-model="input.extra"
label="Output File path" :label="'Extra Configurations'"
type="text"
/> />
<br>
<button type="submit"> <button type="submit">
Submit Submit
</button> </button>
</form> </form>
<pre> {{ input }} </pre>
<h3> Response from the server </h3>
<div> {{ response_message }}</div>
</div> </div>
</template> </template>
<script> <script>
import BaseInput from '@/components/BaseInput.vue' import BaseInput from '@/components/BaseInput.vue'
import BaseCheckbox from '@/components/BaseCheckbox.vue' import BaseCheckbox from '@/components/BaseCheckbox.vue'
import BaseSelect from '@/components/BaseSelect.vue'
import ApiService from '../services/ApiService.js' import ApiService from '../services/ApiService.js'
import UploadFile from '@/components/UploadFile.vue'
import { uuid } from 'vue-uuid'
export default { export default {
components: { components: {
BaseInput, BaseInput,
BaseCheckbox BaseCheckbox,
UploadFile,
BaseSelect,
}, },
data() { data() {
return { return {
@@ -59,31 +65,61 @@ export default {
input: { input: {
verbose: false, verbose: false,
target: '', target: '',
output: '', output: 'output/',
incremental: '', incremental: '',
incrementalStatus: false incrementalStatus: false,
extra: '',
}, },
inputs: [], inputs: [],
response_message : '' response_message: '',
targetList: [],
} }
}, },
methods:{ computed: {
updateOutput() {
return 'output/' + String(this.id) + '.zip'
},
},
created() {
this.fetchTargetList()
this.updateUUID()
},
methods: {
sendForm(e) { sendForm(e) {
// console.log(this.input)
ApiService.postInput(this.input, this.id) ApiService.postInput(this.input, this.id)
.then(Response => { .then((Response) => {
this.response_message = Response.data this.response_message = Response.data
alert(this.response_message)
}) })
.catch(err => { .catch((err) => {
this.response_message = 'Error! ' + err this.response_message = 'Error! ' + err
}) })
this.input = { this.input = {
verbose: false, verbose: false,
target: '', target: '',
output: '', output: 'output/',
incremental: '', incremental: '',
incrementalStatus: false incrementalStatus: false,
extra: '',
} }
} this.updateUUID()
},
async fetchTargetList() {
try {
let response = await ApiService.getFileList('/target')
this.targetList = response.data
} catch (err) {
console.log('Fetch Error', err)
}
},
updateUUID() {
this.id = uuid.v1()
this.input.output += String(this.id) + '.zip'
},
}, },
} }
</script> </script>
<style scoped>
</style>

View File

@@ -1,20 +1,27 @@
""" """
A simple local HTTP server for Android OTA package generation. A local HTTP server for Android OTA package generation.
Based on OTA_from_target_files. Based on OTA_from_target_files.py
Usage:: Usage::
python ./web_server.py [<port>] python ./web_server.py [<port>]
API:: API::
GET /check : check the status of all jobs GET /check : check the status of all jobs
[TODO] GET /check/id : check the status of the job with <id> GET /check/<id> : check the status of the job with <id>
POST /run/id : submit a job with <id>, GET /file : fetch the target file list
GET /download/<id> : download the ota package with <id>
POST /run/<id> : submit a job with <id>,
arguments set in a json uploaded together arguments set in a json uploaded together
[TODO] POST /cancel/id : cancel a job with <id> POST /file/<filename> : upload a target file
[TODO] POST /cancel/<id> : cancel a job with <id>
Other GET request will be redirected to the static request under 'dist' directory
""" """
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, SimpleHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
from threading import Lock from threading import Lock
from ota_interface import ProcessesManagement
import logging import logging
import json import json
import pipes import pipes
@@ -25,123 +32,114 @@ import os
LOCAL_ADDRESS = '0.0.0.0' LOCAL_ADDRESS = '0.0.0.0'
class ThreadSafeContainer: class CORSSimpleHTTPHandler(SimpleHTTPRequestHandler):
def __init__(self): def end_headers(self):
self.__container = {}
self.__lock = Lock()
def set(self, name, value):
with self.__lock:
self.__container[name] = value
def get(self, name):
with self.__lock:
return self.__container[name]
def get_keys(self):
with self.__lock:
return self.__container.keys()
class RequestHandler(BaseHTTPRequestHandler):
def get_status(self):
statusList = []
for id in PROCESSES.get_keys():
status = {}
status['id'] = id
if PROCESSES.get(id).poll() == None:
status['status'] = 'Running'
elif PROCESSES.get(id).poll() == 0:
status['status'] = 'Finished'
else:
status['status'] = 'Error'
statusList.append(json.dumps(status))
return '['+','.join(statusList)+']'
def ota_generate(self, args, id=0):
command = ['ota_from_target_files']
# Check essential configuration is properly set
if not os.path.isfile(args['target']):
raise FileNotFoundError
if not args['output']:
raise SyntaxError
if args['verbose']:
command.append('-v')
command.append('-k')
command.append(
'../../../build/make/target/product/security/testkey')
if args['incremental']:
command.append('-i')
command.append(args['incremental'])
command.append(args['target'])
command.append(args['output'])
stderr_pipes = pipes.Template()
stdout_pipes = pipes.Template()
ferr = stderr_pipes.open('stderr', 'w')
fout = stdout_pipes.open('stdout', 'w')
PROCESSES.set(id, subprocess.Popen(
command, stderr=ferr, stdout=fout))
logging.info(
'Starting generating OTA package with id {}: \n {}'
.format(id, command))
def _set_response(self, type='text/html'):
self.send_response(200)
self.send_header('Content-type', type)
try: try:
origin_address, _ = cgi.parse_header(self.headers['Origin']) origin_address, _ = cgi.parse_header(self.headers['Origin'])
self.send_header('Access-Control-Allow-Credentials', 'true') self.send_header('Access-Control-Allow-Credentials', 'true')
self.send_header('Access-Control-Allow-Origin', origin_address) self.send_header('Access-Control-Allow-Origin', origin_address)
except TypeError: except TypeError:
pass pass
super().end_headers()
class RequestHandler(CORSSimpleHTTPHandler):
def _set_response(self, code=200, type='text/html'):
self.send_response(code)
self.send_header('Content-type', type)
self.end_headers() self.end_headers()
def do_OPTIONS(self): def do_OPTIONS(self):
self.send_response(200) self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS')
self.send_header("Access-Control-Allow-Headers", "X-Requested-With") self.send_header("Access-Control-Allow-Headers", "X-Requested-With")
self.send_header("Access-Control-Allow-Headers", "Content-Type") self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers() self.end_headers()
def do_GET(self): def do_GET(self):
if str(self.path) == '/check': if self.path.startswith('/check'):
status = self.get_status() if self.path == '/check' or self.path == '/check/':
self._set_response('application/json') status = jobs.get_status()
self.wfile.write( self._set_response(type='application/json')
status.encode() self.wfile.write(
json.dumps(status).encode()
)
else:
id = self.path[7:]
status = jobs.get_status_by_ID(id=id, details=True)
self._set_response(type='application/json')
self.wfile.write(
json.dumps(status).encode()
)
logging.info(
"GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
str(self.path), str(self.headers), status
) )
else:
self.send_error(404)
return return
logging.info( elif self.path.startswith('/file'):
"GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n", file_list = jobs.get_list(self.path[6:])
str(self.path), str(self.headers), status self._set_response(type='application/json')
) self.wfile.write(
json.dumps(file_list).encode()
)
logging.info(
"GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
str(self.path), str(self.headers), file_list
)
return
elif self.path.startswith('/download'):
self.path = self.path[10:]
return CORSSimpleHTTPHandler.do_GET(self)
else:
self.path = '/dist' + self.path
return CORSSimpleHTTPHandler.do_GET(self)
def do_POST(self): def do_POST(self):
content_type, _ = cgi.parse_header(self.headers['content-type']) if self.path.startswith('/run'):
if content_type != 'application/json': content_type, _ = cgi.parse_header(self.headers['content-type'])
self.send_response(400) if content_type != 'application/json':
self.end_headers() self.send_response(400)
return self.end_headers()
content_length = int(self.headers['Content-Length']) return
post_data = json.loads(self.rfile.read(content_length)) content_length = int(self.headers['Content-Length'])
if str(self.path)[:4] == '/run': post_data = json.loads(self.rfile.read(content_length))
try: try:
self.ota_generate(post_data, id=str(self.path[5:])) jobs.ota_generate(post_data, id=str(self.path[5:]))
self._set_response() self._set_response(code=201)
self.wfile.write( self.wfile.write(
"ota generator start running".encode('utf-8')) "ota generator start running".encode('utf-8'))
except SyntaxError: except SyntaxError:
self.send_error(400) self.send_error(400)
logging.info(
"POST request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
str(self.path), str(self.headers),
json.dumps(post_data)
)
elif self.path.startswith('/file'):
file_name = os.path.join('target', self.path[6:])
file_length = int(self.headers['Content-Length'])
with open(file_name, 'wb') as output_file:
# Unwrap the uploaded file first (due to the usage of FormData)
# The wrapper has a boundary line at the top and bottom
# and some file information in the beginning
# There are a file content line, a file name line, and an empty line
# The boundary line in the bottom is 4 bytes longer than the top one
# Please refer to the following links for more details:
# https://stackoverflow.com/questions/8659808/how-does-http-file-upload-work
# https://datatracker.ietf.org/doc/html/rfc1867
upper_boundary = self.rfile.readline()
file_length -= len(upper_boundary) * 2 + 4
file_length -= len(self.rfile.readline())
file_length -= len(self.rfile.readline())
file_length -= len(self.rfile.readline())
output_file.write(self.rfile.read(file_length))
self._set_response(code=201)
self.wfile.write(
"File received, saved into {}".format(
file_name).encode('utf-8')
)
else: else:
self.send_error(400) self.send_error(400)
logging.info(
"POST request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
str(self.path), str(self.headers),
json.dumps(post_data)
)
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
@@ -166,7 +164,7 @@ def run_server(SeverClass=ThreadedHTTPServer, HandlerClass=RequestHandler, port=
if __name__ == '__main__': if __name__ == '__main__':
from sys import argv from sys import argv
print(argv) print(argv)
PROCESSES = ThreadSafeContainer() jobs = ProcessesManagement()
if len(argv) == 2: if len(argv) == 2:
run_server(port=int(argv[1])) run_server(port=int(argv[1]))
else: else: