initial commit
This commit is contained in:
commit
d3e5b28e72
15 files changed
+34273
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
build
|
||||||
|
*.o
|
||||||
|
.cache
|
||||||
|
up
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
cmake_minimum_required (VERSION 3.6)
|
||||||
|
project(up LANGUAGES C)
|
||||||
|
|
||||||
|
set(CMAKE_C_STANDARD 23)
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
include_directories(./src/lib/ ./src/)
|
||||||
|
FILE(GLOB_RECURSE SOURCES src/*.c src/lib/*.c)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
up
|
||||||
|
${SOURCES}
|
||||||
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
CC = gcc
|
||||||
|
CFLAGS = -Wall -g -std=gnu2x
|
||||||
|
LDFLAGS =
|
||||||
|
LIBS =
|
||||||
|
INCLUDES = -I./src/lib -I./src
|
||||||
|
SRCS = $(wildcard src/*.c src/lib/*.c)
|
||||||
|
OBJS = $(SRCS:.c=.o)
|
||||||
|
TARGET = up
|
||||||
|
CROSS_COMPILE =
|
||||||
|
DESTDIR = /
|
||||||
|
|
||||||
|
all: $(TARGET)
|
||||||
|
|
||||||
|
$(TARGET): $(OBJS)
|
||||||
|
$(CROSS_COMPILE)$(CC) $(LDFLAGS) $^ -o $@ $(LIBS)
|
||||||
|
|
||||||
|
%.o: %.c
|
||||||
|
$(CROSS_COMPILE)$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(OBJS) $(TARGET)
|
||||||
|
|
||||||
|
install:
|
||||||
|
if [ ! -d $(DESTDIR)/etc/finit.d/available/ ]; then \
|
||||||
|
mkdir -p $(DESTDIR)/etc/finit.d/available/; \
|
||||||
|
fi
|
||||||
|
if [ ! -d $(DESTDIR)/etc/finit.d/enabled/ ]; then \
|
||||||
|
mkdir -p $(DESTDIR)/etc/finit.d/enabled/; \
|
||||||
|
fi
|
||||||
|
if [ ! -d $(DESTDIR)/srv/www/ ]; then \
|
||||||
|
mkdir -p $(DESTDIR)/srv/www/; \
|
||||||
|
fi
|
||||||
|
cp -v web/* up $(DESTDIR)/srv/www/
|
||||||
|
cp -v up.conf $(DESTDIR)/etc/finit.d/available/
|
||||||
|
ln -svf ../available/up.conf $(DESTDIR)/etc/finit.d/enabled/
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Bootloader Update Manager
|
||||||
|
|
||||||
|
A C-based web application for updating kernel, device-tree, and U-Boot environment files on Orange Pi Zero 2W.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Web Interface**: Modern, responsive UI for easy file management
|
||||||
|
- **Kernel Update**: Upload `Image.gz` to boot partition
|
||||||
|
- **Device-Tree Update**: Upload `sun50i-h618-orangepi-zero2w.dtb`
|
||||||
|
- **U-Boot Environment**: Upload `uboot.env`
|
||||||
|
- **extlinux.conf Editor**: View and edit boot configuration
|
||||||
|
- **Progress Bar**: Real-time upload progress tracking
|
||||||
|
- **Auto-mount**: Automatically mounts `/dev/mmcblk1p1` to `/media`
|
||||||
|
- **Large File Support**: Upload files up to 200MB
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- GCC compiler
|
||||||
|
- [Mongoose](https://mongoose.ws) - Embedded web server
|
||||||
|
- [cJSON](https://github.com/DaveGamble/cJSON) - JSON parser
|
||||||
|
- Root privileges (for mounting devices)
|
||||||
|
|
||||||
|
### Installing Dependencies
|
||||||
|
|
||||||
|
**Ubuntu/Debian:**
|
||||||
|
```bash
|
||||||
|
# Install mongoose (or build from source)
|
||||||
|
sudo apt-get install libmongoose-dev
|
||||||
|
|
||||||
|
# Install cJSON
|
||||||
|
sudo apt-get install libcjson-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build from source:**
|
||||||
|
```bash
|
||||||
|
# Mongoose
|
||||||
|
git clone https://github.com/cesanta/mongoose.git
|
||||||
|
cd mongoose && make && sudo make install
|
||||||
|
|
||||||
|
# cJSON
|
||||||
|
git clone https://github.com/DaveGamble/cJSON.git
|
||||||
|
cd cJSON && mkdir build && cd build && cmake .. && make && sudo make install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo make run
|
||||||
|
# or
|
||||||
|
sudo ./up
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will start on **http://localhost:8080**
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/api/upload` | POST | Upload files (kernel, dtb, uboot) |
|
||||||
|
| `/api/extlinux` | GET | Read extlinux.conf |
|
||||||
|
| `/api/extlinux` | POST | Write extlinux.conf |
|
||||||
|
| `/api/progress` | GET | Get upload progress |
|
||||||
|
| `/api/mount` | GET | Get mount status |
|
||||||
|
|
||||||
|
## Upload Parameters
|
||||||
|
|
||||||
|
For `/api/upload`:
|
||||||
|
- `type`: File type (`kernel`, `dtb`, or `uboot`)
|
||||||
|
- `file`: The file to upload
|
||||||
|
|
||||||
|
## File Locations
|
||||||
|
|
||||||
|
| Type | Destination File |
|
||||||
|
|------|------------------|
|
||||||
|
| Kernel | `/media/Image.gz` |
|
||||||
|
| Device-Tree | `/media/sun50i-h618-orangepi-zero2w.dtb` |
|
||||||
|
| U-Boot Env | `/media/uboot.env` |
|
||||||
|
| Extlinux | `/media/extlinux.conf` |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── main.c # Main application (C server)
|
||||||
|
├── index.html # Web interface HTML
|
||||||
|
├── style.css # Web interface styles
|
||||||
|
├── app.js # Frontend JavaScript
|
||||||
|
├── Makefile # Build configuration
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
The web interface includes:
|
||||||
|
- System status panel showing mount state
|
||||||
|
- Tabbed upload interface for different file types
|
||||||
|
- Real-time progress bar during uploads
|
||||||
|
- Full-featured text editor for extlinux.conf
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
⚠️ **Warning**: This application requires root privileges to mount devices. Use with caution and ensure proper access controls in production environments.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
project('up', 'c', default_options: ['cpp_std=c20'])
|
||||||
|
include_dirs = include_directories('src', 'src/lib')
|
||||||
|
executable(
|
||||||
|
'up',
|
||||||
|
'src/main.c',
|
||||||
|
'src/lib/cJSON.c',
|
||||||
|
'src/lib/mongoose.c',
|
||||||
|
include_directories: include_dirs,
|
||||||
|
install: true,
|
||||||
|
)
|
||||||
+3191
File diff suppressed because it is too large.
Load diff
+306
@@ -0,0 +1,306 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef cJSON__h
|
||||||
|
#define cJSON__h
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
|
||||||
|
#define __WINDOWS__
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __WINDOWS__
|
||||||
|
|
||||||
|
/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options:
|
||||||
|
|
||||||
|
CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
|
||||||
|
CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
|
||||||
|
CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
|
||||||
|
|
||||||
|
For *nix builds that support visibility attribute, you can define similar behavior by
|
||||||
|
|
||||||
|
setting default visibility to hidden by adding
|
||||||
|
-fvisibility=hidden (for gcc)
|
||||||
|
or
|
||||||
|
-xldscope=hidden (for sun cc)
|
||||||
|
to CFLAGS
|
||||||
|
|
||||||
|
then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define CJSON_CDECL __cdecl
|
||||||
|
#define CJSON_STDCALL __stdcall
|
||||||
|
|
||||||
|
/* export symbols by default, this is necessary for copy pasting the C and header file */
|
||||||
|
#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
|
||||||
|
#define CJSON_EXPORT_SYMBOLS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(CJSON_HIDE_SYMBOLS)
|
||||||
|
#define CJSON_PUBLIC(type) type CJSON_STDCALL
|
||||||
|
#elif defined(CJSON_EXPORT_SYMBOLS)
|
||||||
|
#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
|
||||||
|
#elif defined(CJSON_IMPORT_SYMBOLS)
|
||||||
|
#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
|
||||||
|
#endif
|
||||||
|
#else /* !__WINDOWS__ */
|
||||||
|
#define CJSON_CDECL
|
||||||
|
#define CJSON_STDCALL
|
||||||
|
|
||||||
|
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
|
||||||
|
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
|
||||||
|
#else
|
||||||
|
#define CJSON_PUBLIC(type) type
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* project version */
|
||||||
|
#define CJSON_VERSION_MAJOR 1
|
||||||
|
#define CJSON_VERSION_MINOR 7
|
||||||
|
#define CJSON_VERSION_PATCH 18
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* cJSON Types: */
|
||||||
|
#define cJSON_Invalid (0)
|
||||||
|
#define cJSON_False (1 << 0)
|
||||||
|
#define cJSON_True (1 << 1)
|
||||||
|
#define cJSON_NULL (1 << 2)
|
||||||
|
#define cJSON_Number (1 << 3)
|
||||||
|
#define cJSON_String (1 << 4)
|
||||||
|
#define cJSON_Array (1 << 5)
|
||||||
|
#define cJSON_Object (1 << 6)
|
||||||
|
#define cJSON_Raw (1 << 7) /* raw json */
|
||||||
|
|
||||||
|
#define cJSON_IsReference 256
|
||||||
|
#define cJSON_StringIsConst 512
|
||||||
|
|
||||||
|
/* The cJSON structure: */
|
||||||
|
typedef struct cJSON
|
||||||
|
{
|
||||||
|
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
|
||||||
|
struct cJSON *next;
|
||||||
|
struct cJSON *prev;
|
||||||
|
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
|
||||||
|
struct cJSON *child;
|
||||||
|
|
||||||
|
/* The type of the item, as above. */
|
||||||
|
int type;
|
||||||
|
|
||||||
|
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
|
||||||
|
char *valuestring;
|
||||||
|
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
|
||||||
|
int valueint;
|
||||||
|
/* The item's number, if type==cJSON_Number */
|
||||||
|
double valuedouble;
|
||||||
|
|
||||||
|
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
|
||||||
|
char *string;
|
||||||
|
} cJSON;
|
||||||
|
|
||||||
|
typedef struct cJSON_Hooks
|
||||||
|
{
|
||||||
|
/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
|
||||||
|
void *(CJSON_CDECL *malloc_fn)(size_t sz);
|
||||||
|
void (CJSON_CDECL *free_fn)(void *ptr);
|
||||||
|
} cJSON_Hooks;
|
||||||
|
|
||||||
|
typedef int cJSON_bool;
|
||||||
|
|
||||||
|
/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
|
||||||
|
* This is to prevent stack overflows. */
|
||||||
|
#ifndef CJSON_NESTING_LIMIT
|
||||||
|
#define CJSON_NESTING_LIMIT 1000
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Limits the length of circular references can be before cJSON rejects to parse them.
|
||||||
|
* This is to prevent stack overflows. */
|
||||||
|
#ifndef CJSON_CIRCULAR_LIMIT
|
||||||
|
#define CJSON_CIRCULAR_LIMIT 10000
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* returns the version of cJSON as a string */
|
||||||
|
CJSON_PUBLIC(const char*) cJSON_Version(void);
|
||||||
|
|
||||||
|
/* Supply malloc, realloc and free functions to cJSON */
|
||||||
|
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
|
||||||
|
|
||||||
|
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
|
||||||
|
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
|
||||||
|
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
|
||||||
|
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
|
||||||
|
|
||||||
|
/* Render a cJSON entity to text for transfer/storage. */
|
||||||
|
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
|
||||||
|
/* Render a cJSON entity to text for transfer/storage without any formatting. */
|
||||||
|
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
|
||||||
|
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
|
||||||
|
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
|
||||||
|
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
|
||||||
|
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
|
||||||
|
/* Delete a cJSON entity and all subentities. */
|
||||||
|
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
|
||||||
|
|
||||||
|
/* Returns the number of items in an array (or object). */
|
||||||
|
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
|
||||||
|
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
|
||||||
|
/* Get item "string" from object. Case insensitive. */
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
|
||||||
|
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
|
||||||
|
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
|
||||||
|
|
||||||
|
/* Check item type and return its value */
|
||||||
|
CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
|
||||||
|
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);
|
||||||
|
|
||||||
|
/* These functions check the type of an item */
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
|
||||||
|
|
||||||
|
/* These calls create a cJSON item of the appropriate type. */
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
|
||||||
|
/* raw json */
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
|
||||||
|
|
||||||
|
/* Create a string where valuestring references a string so
|
||||||
|
* it will not be freed by cJSON_Delete */
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
|
||||||
|
/* Create an object/array that only references it's elements so
|
||||||
|
* they will not be freed by cJSON_Delete */
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
|
||||||
|
|
||||||
|
/* These utilities create an Array of count items.
|
||||||
|
* The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
|
||||||
|
|
||||||
|
/* Append item to the specified array/object. */
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
|
||||||
|
/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
|
||||||
|
* WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
|
||||||
|
* writing to `item->string` */
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
|
||||||
|
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
|
||||||
|
|
||||||
|
/* Remove/Detach items from Arrays/Objects. */
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
|
||||||
|
CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
|
||||||
|
CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
|
||||||
|
CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
|
||||||
|
|
||||||
|
/* Update array items. */
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
|
||||||
|
|
||||||
|
/* Duplicate a cJSON item */
|
||||||
|
CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
|
||||||
|
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
|
||||||
|
* need to be released. With recurse!=0, it will duplicate any children connected to the item.
|
||||||
|
* The item->next and ->prev pointers are always zero on return from Duplicate. */
|
||||||
|
/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
|
||||||
|
* case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
|
||||||
|
CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
|
||||||
|
|
||||||
|
/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
|
||||||
|
* The input pointer json cannot point to a read-only address area, such as a string constant,
|
||||||
|
* but should point to a readable and writable address area. */
|
||||||
|
CJSON_PUBLIC(void) cJSON_Minify(char *json);
|
||||||
|
|
||||||
|
/* Helper functions for creating and adding items to an object at the same time.
|
||||||
|
* They return the added item or NULL on failure. */
|
||||||
|
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
|
||||||
|
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
|
||||||
|
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
|
||||||
|
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
|
||||||
|
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
|
||||||
|
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
|
||||||
|
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
|
||||||
|
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
|
||||||
|
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
|
||||||
|
|
||||||
|
/* When assigning an integer value, it needs to be propagated to valuedouble too. */
|
||||||
|
#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
|
||||||
|
/* helper for the cJSON_SetNumberValue macro */
|
||||||
|
CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
|
||||||
|
#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
|
||||||
|
/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
|
||||||
|
CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
|
||||||
|
|
||||||
|
/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
|
||||||
|
#define cJSON_SetBoolValue(object, boolValue) ( \
|
||||||
|
(object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \
|
||||||
|
(object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \
|
||||||
|
cJSON_Invalid\
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Macro for iterating over an array or object */
|
||||||
|
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
|
||||||
|
|
||||||
|
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
|
||||||
|
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
|
||||||
|
CJSON_PUBLIC(void) cJSON_free(void *object);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
+25464
File diff suppressed because it is too large.
Load diff
+3780
File diff suppressed because it is too large.
Load diff
+456
@@ -0,0 +1,456 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/mount.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
|
||||||
|
#include "mongoose_config.h"
|
||||||
|
#include "mongoose.h"
|
||||||
|
#include "cJSON.h"
|
||||||
|
|
||||||
|
#define MOUNT_POINT "/media"
|
||||||
|
#define DEVICE "/dev/mmcblk1p1"
|
||||||
|
#define KERNEL_FILE "Image.gz"
|
||||||
|
#define DTB_FILE "sun50i-h618-orangepi-zero2w.dtb"
|
||||||
|
#define UBOOT_FILE "uboot.env"
|
||||||
|
#define EXTLINUX_FILE "extlinux/extlinux.conf"
|
||||||
|
|
||||||
|
#define MAX_FILE_SIZE (200 * 1024 * 1024) // 200 MB
|
||||||
|
|
||||||
|
static const char *s_http_port = "http://0.0.0.0:43243";
|
||||||
|
static struct mg_mgr mgr;
|
||||||
|
|
||||||
|
// Upload session state for streaming
|
||||||
|
typedef struct {
|
||||||
|
char type[32];
|
||||||
|
char filepath[512];
|
||||||
|
FILE *fp;
|
||||||
|
size_t received;
|
||||||
|
int progress;
|
||||||
|
char status[64];
|
||||||
|
int uploading;
|
||||||
|
} UploadSession;
|
||||||
|
|
||||||
|
static UploadSession g_upload = {0};
|
||||||
|
|
||||||
|
// Helper: compare mg_str with C string
|
||||||
|
static int mg_str_eq(struct mg_str s, const char *cstr) {
|
||||||
|
return mg_strcmp(s, mg_str(cstr)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: copy mg_str to C string
|
||||||
|
static void mg_str_cpy(struct mg_str s, char *dst, size_t max) {
|
||||||
|
size_t len = s.len < max - 1 ? s.len : max - 1;
|
||||||
|
memcpy(dst, s.buf, len);
|
||||||
|
dst[len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: match URI
|
||||||
|
static int mg_match_uri(struct mg_str uri, const char *pattern) {
|
||||||
|
return mg_strcmp(uri, mg_str(pattern)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
static int ensure_dir(const char *path) {
|
||||||
|
struct stat st;
|
||||||
|
if (stat(path, &st) == -1) {
|
||||||
|
if (mkdir(path, 0755) == -1) {
|
||||||
|
perror("mkdir");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount the device
|
||||||
|
static int mount_device(void) {
|
||||||
|
if (ensure_dir(MOUNT_POINT) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already mounted
|
||||||
|
FILE *f = fopen("/proc/mounts", "r");
|
||||||
|
if (f) {
|
||||||
|
char line[256];
|
||||||
|
while (fgets(line, sizeof(line), f)) {
|
||||||
|
if (strstr(line, MOUNT_POINT)) {
|
||||||
|
fclose(f);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mount(DEVICE, MOUNT_POINT, "vfat", 0, NULL) != 0 &&
|
||||||
|
mount(DEVICE, MOUNT_POINT, "ext4", 0, NULL) != 0) {
|
||||||
|
perror("mount");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get target filename based on type
|
||||||
|
static const char* get_target_filename(const char *type) {
|
||||||
|
if (strcmp(type, "kernel") == 0) return KERNEL_FILE;
|
||||||
|
if (strcmp(type, "dtb") == 0) return DTB_FILE;
|
||||||
|
if (strcmp(type, "uboot") == 0) return UBOOT_FILE;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get MIME type based on file extension
|
||||||
|
static const char* get_mime_type(const char *path) {
|
||||||
|
const char *ext = strrchr(path, '.');
|
||||||
|
if (ext == NULL) return "text/plain";
|
||||||
|
if (mg_strcasecmp(mg_str(ext), mg_str(".html")) == 0) return "text/html; charset=utf-8";
|
||||||
|
if (mg_strcasecmp(mg_str(ext), mg_str(".css")) == 0) return "text/css; charset=utf-8";
|
||||||
|
if (mg_strcasecmp(mg_str(ext), mg_str(".js")) == 0) return "application/javascript; charset=utf-8";
|
||||||
|
if (mg_strcasecmp(mg_str(ext), mg_str(".json")) == 0) return "application/json; charset=utf-8";
|
||||||
|
if (mg_strcasecmp(mg_str(ext), mg_str(".png")) == 0) return "image/png";
|
||||||
|
if (mg_strcasecmp(mg_str(ext), mg_str(".jpg")) == 0 || mg_strcasecmp(mg_str(ext), mg_str(".jpeg")) == 0) return "image/jpeg";
|
||||||
|
if (mg_strcasecmp(mg_str(ext), mg_str(".gif")) == 0) return "image/gif";
|
||||||
|
if (mg_strcasecmp(mg_str(ext), mg_str(".svg")) == 0) return "image/svg+xml";
|
||||||
|
if (mg_strcasecmp(mg_str(ext), mg_str(".ico")) == 0) return "image/x-icon";
|
||||||
|
if (mg_strcasecmp(mg_str(ext), mg_str(".txt")) == 0) return "text/plain; charset=utf-8";
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve static files
|
||||||
|
static void handle_static(struct mg_connection *c, const char *path) {
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) {
|
||||||
|
mg_http_reply(c, 404, "", "File not found\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long size = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
|
||||||
|
char *content = malloc(size);
|
||||||
|
if (!content) {
|
||||||
|
fclose(f);
|
||||||
|
mg_http_reply(c, 500, "", "Memory error\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fread(content, 1, size, f);
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
const char *mime = get_mime_type(path);
|
||||||
|
mg_printf(c, "HTTP/1.1 200 OK\r\n");
|
||||||
|
mg_printf(c, "Content-Type: %s\r\n", mime);
|
||||||
|
mg_printf(c, "Content-Length: %ld\r\n", size);
|
||||||
|
mg_printf(c, "Connection: close\r\n");
|
||||||
|
mg_printf(c, "\r\n");
|
||||||
|
mg_send(c, content, size);
|
||||||
|
free(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle extlinux.conf read
|
||||||
|
static void handle_extlinux_read(struct mg_connection *c) {
|
||||||
|
if (mount_device() < 0) {
|
||||||
|
mg_http_reply(c, 500, "", "{\"error\":\"Failed to mount device\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char filepath[512];
|
||||||
|
snprintf(filepath, sizeof(filepath), "%s/%s", MOUNT_POINT, EXTLINUX_FILE);
|
||||||
|
|
||||||
|
FILE *f = fopen(filepath, "r");
|
||||||
|
if (!f) {
|
||||||
|
mg_http_reply(c, 404, "", "{\"error\":\"extlinux.conf not found\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long size = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
|
||||||
|
char *content = malloc(size + 1);
|
||||||
|
if (!content) {
|
||||||
|
fclose(f);
|
||||||
|
mg_http_reply(c, 500, "", "{\"error\":\"Memory allocation failed\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fread(content, 1, size, f);
|
||||||
|
content[size] = '\0';
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(root, "status", "success");
|
||||||
|
cJSON_AddStringToObject(root, "content", content);
|
||||||
|
cJSON_AddNumberToObject(root, "size", size);
|
||||||
|
|
||||||
|
char *json = cJSON_PrintUnformatted(root);
|
||||||
|
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\n", json);
|
||||||
|
free(json);
|
||||||
|
free(content);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle extlinux.conf write
|
||||||
|
static void handle_extlinux_write(struct mg_connection *c, struct mg_http_message *hm) {
|
||||||
|
char body[4096] = {0};
|
||||||
|
size_t body_len = hm->body.len < sizeof(body) - 1 ? hm->body.len : sizeof(body) - 1;
|
||||||
|
memcpy(body, hm->body.buf, body_len);
|
||||||
|
|
||||||
|
cJSON *root = cJSON_Parse(body);
|
||||||
|
if (!root) {
|
||||||
|
mg_http_reply(c, 400, "", "{\"error\":\"Invalid JSON\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *content = cJSON_GetObjectItem(root, "content");
|
||||||
|
if (!content || !cJSON_IsString(content)) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
mg_http_reply(c, 400, "", "{\"error\":\"Missing content field\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mount_device() < 0) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
mg_http_reply(c, 500, "", "{\"error\":\"Failed to mount device\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char filepath[512];
|
||||||
|
snprintf(filepath, sizeof(filepath), "%s/%s", MOUNT_POINT, EXTLINUX_FILE);
|
||||||
|
|
||||||
|
FILE *f = fopen(filepath, "w");
|
||||||
|
if (!f) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
mg_http_reply(c, 500, "", "{\"error\":\"Failed to open file\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(f, "%s", content->valuestring);
|
||||||
|
fclose(f);
|
||||||
|
sync();
|
||||||
|
|
||||||
|
mg_http_reply(c, 200, "Content-Type: application/json\r\n",
|
||||||
|
"{\"status\":\"success\",\"message\":\"extlinux.conf updated\"}\n");
|
||||||
|
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle upload progress
|
||||||
|
static void handle_progress(struct mg_connection *c) {
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(root, "status", "success");
|
||||||
|
cJSON_AddStringToObject(root, "type", g_upload.type);
|
||||||
|
cJSON_AddNumberToObject(root, "progress", g_upload.progress);
|
||||||
|
cJSON_AddNumberToObject(root, "total", g_upload.received);
|
||||||
|
cJSON_AddNumberToObject(root, "received", g_upload.received);
|
||||||
|
cJSON_AddStringToObject(root, "upload_status", g_upload.status);
|
||||||
|
|
||||||
|
char *json = cJSON_PrintUnformatted(root);
|
||||||
|
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\n", json);
|
||||||
|
free(json);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle mount status
|
||||||
|
static void handle_mount_status(struct mg_connection *c) {
|
||||||
|
int mounted = 0;
|
||||||
|
FILE *f = fopen("/proc/mounts", "r");
|
||||||
|
if (f) {
|
||||||
|
char line[256];
|
||||||
|
while (fgets(line, sizeof(line), f)) {
|
||||||
|
if (strstr(line, MOUNT_POINT)) {
|
||||||
|
mounted = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
cJSON_AddBoolToObject(root, "mounted", mounted);
|
||||||
|
cJSON_AddStringToObject(root, "mount_point", MOUNT_POINT);
|
||||||
|
cJSON_AddStringToObject(root, "device", DEVICE);
|
||||||
|
|
||||||
|
char *json = cJSON_PrintUnformatted(root);
|
||||||
|
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\n", json);
|
||||||
|
free(json);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle system reboot
|
||||||
|
static void handle_reboot(struct mg_connection *c) {
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(root, "status", "success");
|
||||||
|
cJSON_AddStringToObject(root, "message", "System rebooting...");
|
||||||
|
|
||||||
|
char *json = cJSON_PrintUnformatted(root);
|
||||||
|
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\n", json);
|
||||||
|
free(json);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
|
||||||
|
// Sync all filesystems
|
||||||
|
sync();
|
||||||
|
|
||||||
|
// Unmount the boot partition if mounted
|
||||||
|
printf("Unmounting %s...\n", MOUNT_POINT);
|
||||||
|
fflush(stdout);
|
||||||
|
umount(MOUNT_POINT);
|
||||||
|
|
||||||
|
// Sync again to ensure all data is written
|
||||||
|
sync();
|
||||||
|
|
||||||
|
// Soft reboot using shutdown command
|
||||||
|
// This sends SIGTERM to all processes and allows finit to shut down gracefully
|
||||||
|
printf("Initiating soft reboot via shutdown...\n");
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
// Use shutdown command for graceful reboot
|
||||||
|
// -r = reboot, -t 0 = no delay, -h now = halt now (alternative)
|
||||||
|
system("shutdown -r now");
|
||||||
|
}
|
||||||
|
|
||||||
|
// File upload handler with streaming support
|
||||||
|
static void fn(struct mg_connection *c, int ev, void *ev_data) {
|
||||||
|
|
||||||
|
if (ev == MG_EV_HTTP_MSG) {
|
||||||
|
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
||||||
|
|
||||||
|
if (mg_match_uri(hm->uri, "/api/upload")) {
|
||||||
|
// Parse multipart to get type
|
||||||
|
char type[32] = {0};
|
||||||
|
struct mg_http_part part;
|
||||||
|
size_t offset = mg_http_next_multipart(hm->body, 0, &part);
|
||||||
|
if (offset > 0 && mg_str_eq(part.name, "type")) {
|
||||||
|
mg_str_cpy(part.body, type, sizeof(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file data
|
||||||
|
offset = 0;
|
||||||
|
struct mg_str file_data = {NULL, 0};
|
||||||
|
while ((offset = mg_http_next_multipart(hm->body, offset, &part)) > 0) {
|
||||||
|
if (mg_str_eq(part.name, "file")) {
|
||||||
|
file_data = part.body;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen(type) == 0 || file_data.len == 0) {
|
||||||
|
mg_http_reply(c, 400, "", "{\"error\":\"Invalid upload data\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_data.len > MAX_FILE_SIZE) {
|
||||||
|
mg_http_reply(c, 400, "", "{\"error\":\"File too large (max 200MB)\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mount_device() < 0) {
|
||||||
|
mg_http_reply(c, 500, "", "{\"error\":\"Failed to mount device\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *filename = get_target_filename(type);
|
||||||
|
if (!filename) {
|
||||||
|
mg_http_reply(c, 400, "", "{\"error\":\"Invalid upload type\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char filepath[512];
|
||||||
|
snprintf(filepath, sizeof(filepath), "%s/%s", MOUNT_POINT, filename);
|
||||||
|
|
||||||
|
printf("Upload: %s (%zu bytes) -> %s\n", type, file_data.len, filepath);
|
||||||
|
|
||||||
|
// Write file in chunks
|
||||||
|
FILE *fp = fopen(filepath, "wb");
|
||||||
|
if (!fp) {
|
||||||
|
mg_http_reply(c, 500, "", "{\"error\":\"Failed to create file\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t written = 0;
|
||||||
|
size_t chunk_size = 65536;
|
||||||
|
while (written < file_data.len) {
|
||||||
|
size_t to_write = (file_data.len - written < chunk_size) ?
|
||||||
|
(file_data.len - written) : chunk_size;
|
||||||
|
if (fwrite(file_data.buf + written, 1, to_write, fp) != to_write) {
|
||||||
|
fclose(fp);
|
||||||
|
mg_http_reply(c, 500, "", "{\"error\":\"Failed to write file\"}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
written += to_write;
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(fp);
|
||||||
|
sync();
|
||||||
|
|
||||||
|
printf("Upload complete: %s\n", filepath);
|
||||||
|
|
||||||
|
memcpy(g_upload.type, type, strlen(type) + 1);
|
||||||
|
g_upload.received = file_data.len;
|
||||||
|
g_upload.progress = 100;
|
||||||
|
snprintf(g_upload.status, sizeof(g_upload.status), "Complete");
|
||||||
|
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(root, "status", "success");
|
||||||
|
cJSON_AddStringToObject(root, "type", type);
|
||||||
|
cJSON_AddStringToObject(root, "filename", filename);
|
||||||
|
cJSON_AddNumberToObject(root, "size", file_data.len);
|
||||||
|
cJSON_AddNumberToObject(root, "progress", 100);
|
||||||
|
|
||||||
|
char *json = cJSON_PrintUnformatted(root);
|
||||||
|
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\n", json);
|
||||||
|
free(json);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
|
||||||
|
} else if (mg_match_uri(hm->uri, "/api/extlinux")) {
|
||||||
|
if (mg_str_eq(hm->method, "GET")) {
|
||||||
|
handle_extlinux_read(c);
|
||||||
|
} else if (mg_str_eq(hm->method, "POST")) {
|
||||||
|
handle_extlinux_write(c, hm);
|
||||||
|
}
|
||||||
|
} else if (mg_match_uri(hm->uri, "/api/progress")) {
|
||||||
|
handle_progress(c);
|
||||||
|
} else if (mg_match_uri(hm->uri, "/api/mount")) {
|
||||||
|
handle_mount_status(c);
|
||||||
|
} else if (mg_match_uri(hm->uri, "/api/reboot")) {
|
||||||
|
handle_reboot(c);
|
||||||
|
} else if (mg_match_uri(hm->uri, "/")) {
|
||||||
|
handle_static(c, "/srv/www/index.html");
|
||||||
|
} else if (mg_match_uri(hm->uri, "/style.css")) {
|
||||||
|
handle_static(c, "/srv/www/style.css");
|
||||||
|
} else if (mg_match_uri(hm->uri, "/app.js")) {
|
||||||
|
handle_static(c, "/srv/www/app.js");
|
||||||
|
} else {
|
||||||
|
mg_http_reply(c, 404, "", "Not found\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("Starting bootloader update server on port %s...\n", s_http_port);
|
||||||
|
printf("Mount point: %s\n", MOUNT_POINT);
|
||||||
|
printf("Device: %s\n", DEVICE);
|
||||||
|
printf("Max file size: %d MB\n", MAX_FILE_SIZE / (1024 * 1024));
|
||||||
|
|
||||||
|
mg_mgr_init(&mgr);
|
||||||
|
|
||||||
|
struct mg_connection *c = mg_http_listen(&mgr, s_http_port, fn, NULL);
|
||||||
|
if (!c) {
|
||||||
|
perror("mg_http_listen");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Server started. Open http://localhost:43243 in your browser\n");
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
mg_mgr_poll(&mgr, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
mg_mgr_free(&mgr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#ifndef MONGOOSE_CONFIG_H
|
||||||
|
#define MONGOOSE_CONFIG_H
|
||||||
|
|
||||||
|
// Increase maximum receive buffer size to 256MB for large file uploads
|
||||||
|
#define MG_MAX_RECV_SIZE (256 * 1024 * 1024)
|
||||||
|
|
||||||
|
// Enable directory listing
|
||||||
|
#define MG_ENABLE_DIRLIST 1
|
||||||
|
|
||||||
|
// Enable filesystem
|
||||||
|
#define MG_ENABLE_FS 1
|
||||||
|
|
||||||
|
// Increase buffer size for better performance
|
||||||
|
#define MG_IO_SIZE 131072 // 128 KB
|
||||||
|
|
||||||
|
#endif
|
||||||
+309
@@ -0,0 +1,309 @@
|
|||||||
|
// Bootloader Update Manager - Frontend JavaScript
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
// Elements
|
||||||
|
const uploadForm = document.getElementById("upload-form");
|
||||||
|
const fileInput = document.getElementById("file-input");
|
||||||
|
const fileName = document.getElementById("file-name");
|
||||||
|
const uploadType = document.getElementById("upload-type");
|
||||||
|
const progressContainer = document.getElementById("progress-container");
|
||||||
|
const progressFill = document.getElementById("progress-fill");
|
||||||
|
const progressPercent = document.getElementById("progress-percent");
|
||||||
|
const progressStatus = document.getElementById("progress-status");
|
||||||
|
const progressBytes = document.getElementById("progress-bytes");
|
||||||
|
const resultMessage = document.getElementById("result-message");
|
||||||
|
const mountStatus = document.getElementById("mount-status");
|
||||||
|
const extlinuxEditor = document.getElementById("extlinux-editor");
|
||||||
|
const editorStatus = document.getElementById("editor-status");
|
||||||
|
|
||||||
|
// Tab buttons
|
||||||
|
const tabBtns = document.querySelectorAll(".tab-btn");
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
checkMountStatus();
|
||||||
|
setInterval(checkMountStatus, 10000); // Check every 10 seconds
|
||||||
|
|
||||||
|
// Tab switching
|
||||||
|
tabBtns.forEach((btn) => {
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
tabBtns.forEach((b) => b.classList.remove("active"));
|
||||||
|
this.classList.add("active");
|
||||||
|
uploadType.value = this.dataset.type;
|
||||||
|
hideResult();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// File input change
|
||||||
|
fileInput.addEventListener("change", function () {
|
||||||
|
if (this.files && this.files[0]) {
|
||||||
|
fileName.textContent = this.files[0].name;
|
||||||
|
} else {
|
||||||
|
fileName.textContent = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drag and drop
|
||||||
|
const fileLabel = document.querySelector(".file-label");
|
||||||
|
|
||||||
|
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
||||||
|
fileLabel.addEventListener(eventName, preventDefaults, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
function preventDefaults(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
["dragenter", "dragover"].forEach((eventName) => {
|
||||||
|
fileLabel.addEventListener(eventName, () => {
|
||||||
|
fileLabel.style.borderColor = "#00d9ff";
|
||||||
|
fileLabel.style.background = "rgba(0,217,255,0.1)";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
["dragleave", "drop"].forEach((eventName) => {
|
||||||
|
fileLabel.addEventListener(eventName, () => {
|
||||||
|
fileLabel.style.borderColor = "";
|
||||||
|
fileLabel.style.background = "";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fileLabel.addEventListener("drop", (e) => {
|
||||||
|
const dt = e.dataTransfer;
|
||||||
|
const files = dt.files;
|
||||||
|
if (files && files[0]) {
|
||||||
|
fileInput.files = files;
|
||||||
|
fileName.textContent = files[0].name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form submission
|
||||||
|
uploadForm.addEventListener("submit", async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
showResult("Please select a file", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size (200MB limit)
|
||||||
|
const MAX_SIZE = 200 * 1024 * 1024;
|
||||||
|
if (file.size > MAX_SIZE) {
|
||||||
|
showResult("File too large. Maximum size is 200MB", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset progress
|
||||||
|
progressContainer.style.display = "block";
|
||||||
|
progressFill.style.width = "0%";
|
||||||
|
progressPercent.textContent = "0%";
|
||||||
|
progressStatus.textContent = "Uploading...";
|
||||||
|
progressBytes.textContent = "";
|
||||||
|
hideResult();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.upload.addEventListener("progress", function (e) {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const percent = Math.round((e.loaded / e.total) * 100);
|
||||||
|
progressFill.style.width = percent + "%";
|
||||||
|
progressPercent.textContent = percent + "%";
|
||||||
|
progressBytes.textContent =
|
||||||
|
formatBytes(e.loaded) + " / " + formatBytes(e.total);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener("load", function () {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const response = JSON.parse(xhr.responseText);
|
||||||
|
progressStatus.textContent = "Complete!";
|
||||||
|
showResult(
|
||||||
|
`Successfully uploaded ${response.filename} (${formatBytes(response.size)})`,
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let errorMsg = "Upload failed";
|
||||||
|
try {
|
||||||
|
const err = JSON.parse(xhr.responseText);
|
||||||
|
errorMsg = err.error || errorMsg;
|
||||||
|
} catch (e) {}
|
||||||
|
progressStatus.textContent = "Failed";
|
||||||
|
showResult(errorMsg, "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener("error", function () {
|
||||||
|
progressStatus.textContent = "Failed";
|
||||||
|
showResult("Network error", "error");
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.open("POST", "/api/upload");
|
||||||
|
xhr.send(formData);
|
||||||
|
} catch (error) {
|
||||||
|
progressStatus.textContent = "Failed";
|
||||||
|
showResult("Error: " + error.message, "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// extlinux.conf buttons
|
||||||
|
document
|
||||||
|
.getElementById("load-extlinux")
|
||||||
|
.addEventListener("click", loadExtlinux);
|
||||||
|
document
|
||||||
|
.getElementById("save-extlinux")
|
||||||
|
.addEventListener("click", saveExtlinux);
|
||||||
|
document
|
||||||
|
.getElementById("clear-extlinux")
|
||||||
|
.addEventListener("click", function () {
|
||||||
|
extlinuxEditor.value = "";
|
||||||
|
showEditorStatus("Editor cleared", "success");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check mount status
|
||||||
|
async function checkMountStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/mount");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const indicator = mountStatus.querySelector(".status-indicator");
|
||||||
|
const text = mountStatus.querySelector(".status-text");
|
||||||
|
|
||||||
|
if (data.mounted) {
|
||||||
|
indicator.className = "status-indicator mounted";
|
||||||
|
text.textContent = "Mounted";
|
||||||
|
} else {
|
||||||
|
indicator.className = "status-indicator not-mounted";
|
||||||
|
text.textContent = "Not Mounted";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const indicator = mountStatus.querySelector(".status-indicator");
|
||||||
|
const text = mountStatus.querySelector(".status-text");
|
||||||
|
indicator.className = "status-indicator";
|
||||||
|
text.textContent = "Error checking status";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load extlinux.conf
|
||||||
|
async function loadExtlinux() {
|
||||||
|
showEditorStatus("Loading...", "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/extlinux");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === "success") {
|
||||||
|
extlinuxEditor.value = data.content;
|
||||||
|
showEditorStatus(`Loaded ${data.size} bytes`, "success");
|
||||||
|
} else {
|
||||||
|
showEditorStatus(data.error || "Failed to load", "error");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showEditorStatus("Error: " + error.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save extlinux.conf
|
||||||
|
async function saveExtlinux() {
|
||||||
|
const content = extlinuxEditor.value;
|
||||||
|
|
||||||
|
if (!content.trim()) {
|
||||||
|
showEditorStatus("Cannot save empty file", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showEditorStatus("Saving...", "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/extlinux", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ content: content }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === "success") {
|
||||||
|
showEditorStatus("Successfully saved extlinux.conf", "success");
|
||||||
|
} else {
|
||||||
|
showEditorStatus(data.error || "Failed to save", "error");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showEditorStatus("Error: " + error.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reboot button
|
||||||
|
const rebootBtn = document.getElementById("reboot-btn");
|
||||||
|
const rebootMessage = document.getElementById("reboot-message");
|
||||||
|
|
||||||
|
if (rebootBtn) {
|
||||||
|
rebootBtn.addEventListener("click", async function () {
|
||||||
|
// Confirm reboot
|
||||||
|
if (!confirm("Are you sure you want to reboot the system?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rebootBtn.disabled = true;
|
||||||
|
rebootMessage.className = "reboot-message";
|
||||||
|
rebootMessage.style.display = "none";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/reboot", { method: "POST" });
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === "success") {
|
||||||
|
rebootMessage.textContent =
|
||||||
|
"System rebooting... Connection will be lost.";
|
||||||
|
rebootMessage.className = "reboot-message success";
|
||||||
|
rebootMessage.style.display = "block";
|
||||||
|
|
||||||
|
// Wait a bit then show reconnection message
|
||||||
|
setTimeout(() => {
|
||||||
|
rebootMessage.textContent =
|
||||||
|
"Waiting for device to come back online...";
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
rebootMessage.textContent = data.error || "Reboot failed";
|
||||||
|
rebootMessage.className = "reboot-message error";
|
||||||
|
rebootMessage.style.display = "block";
|
||||||
|
rebootBtn.disabled = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
rebootMessage.textContent = "Error: " + error.message;
|
||||||
|
rebootMessage.className = "reboot-message error";
|
||||||
|
rebootMessage.style.display = "block";
|
||||||
|
rebootBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function showResult(message, type) {
|
||||||
|
resultMessage.textContent = message;
|
||||||
|
resultMessage.className = "result-message " + type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideResult() {
|
||||||
|
resultMessage.className = "result-message";
|
||||||
|
resultMessage.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditorStatus(message, type) {
|
||||||
|
editorStatus.textContent = message;
|
||||||
|
editorStatus.className = "editor-status " + type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes === 0) return "0 Bytes";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||||
|
}
|
||||||
|
});
|
||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Bootloader Update Manager</title>
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>🚀 Bootloader Update Manager</h1>
|
||||||
|
<p>
|
||||||
|
Orange Pi Zero 2W - Kernel, Device-Tree & U-Boot Environment
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="status-section">
|
||||||
|
<h2>System Status</h2>
|
||||||
|
<div class="status-card">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="label">Mount Point:</span>
|
||||||
|
<span class="value">/media</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="label">Device:</span>
|
||||||
|
<span class="value">/dev/mmcblk1p1</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="label">Mount Status:</span>
|
||||||
|
<span class="value" id="mount-status">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
<span class="status-text">Checking...</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="upload-section">
|
||||||
|
<h2>File Upload</h2>
|
||||||
|
<div class="upload-tabs">
|
||||||
|
<button class="tab-btn active" data-type="kernel">
|
||||||
|
Kernel (Image.gz)
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-type="dtb">
|
||||||
|
Device-Tree (DTB)
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-type="uboot">
|
||||||
|
U-Boot Environment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-area">
|
||||||
|
<form id="upload-form" enctype="multipart/form-data">
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="type"
|
||||||
|
id="upload-type"
|
||||||
|
value="kernel"
|
||||||
|
/>
|
||||||
|
<div class="file-input-wrapper">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="file"
|
||||||
|
id="file-input"
|
||||||
|
accept=".gz,.dtb,.env,.bin"
|
||||||
|
/>
|
||||||
|
<label for="file-input" class="file-label">
|
||||||
|
<span class="file-icon">📁</span>
|
||||||
|
<span class="file-text"
|
||||||
|
>Drop file here or click to browse</span
|
||||||
|
>
|
||||||
|
<span class="file-name" id="file-name"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="upload-btn">Upload</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="progress-container"
|
||||||
|
id="progress-container"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<div class="progress-header">
|
||||||
|
<span>Upload Progress</span>
|
||||||
|
<span id="progress-percent">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-info">
|
||||||
|
<span id="progress-status">Waiting...</span>
|
||||||
|
<span id="progress-bytes"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-message" id="result-message"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="extlinux-section">
|
||||||
|
<h2>extlinux.conf Editor</h2>
|
||||||
|
<div class="editor-container">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<button id="load-extlinux" class="toolbar-btn">
|
||||||
|
📥 Load
|
||||||
|
</button>
|
||||||
|
<button id="save-extlinux" class="toolbar-btn">
|
||||||
|
💾 Save
|
||||||
|
</button>
|
||||||
|
<button id="clear-extlinux" class="toolbar-btn">
|
||||||
|
🗑️ Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
id="extlinux-editor"
|
||||||
|
placeholder="Load extlinux.conf to edit..."
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
<div class="editor-status" id="editor-status"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="reboot-section">
|
||||||
|
<h2>System Control</h2>
|
||||||
|
<div class="reboot-container">
|
||||||
|
<p class="reboot-warning">
|
||||||
|
⚠️ Warning: This will reboot the device immediately
|
||||||
|
</p>
|
||||||
|
<button id="reboot-btn" class="reboot-btn">
|
||||||
|
🔄 Reboot System
|
||||||
|
</button>
|
||||||
|
<div class="reboot-message" id="reboot-message"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>Bootloader Update Manager © 2026</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+432
@@ -0,0 +1,432 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||||
|
sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #e4e4e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
color: #888;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #00d9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Section */
|
||||||
|
.status-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item .label {
|
||||||
|
display: block;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item .value {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #888;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.mounted {
|
||||||
|
background: #00ff88;
|
||||||
|
box-shadow: 0 0 10px #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.not-mounted {
|
||||||
|
background: #ff4757;
|
||||||
|
box-shadow: 0 0 10px #ff4757;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upload Section */
|
||||||
|
.upload-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #e4e4e4;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
||||||
|
color: #1a1a2e;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-wrapper {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 50px;
|
||||||
|
border: 2px dashed rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-label:hover {
|
||||||
|
border-color: #00d9ff;
|
||||||
|
background: rgba(0, 217, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-text {
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
color: #00ff88;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
padding: 15px 40px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
||||||
|
color: #1a1a2e;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 217, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar */
|
||||||
|
.progress-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message.success {
|
||||||
|
display: block;
|
||||||
|
background: rgba(0, 255, 136, 0.2);
|
||||||
|
border: 1px solid #00ff88;
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message.error {
|
||||||
|
display: block;
|
||||||
|
background: rgba(255, 71, 87, 0.2);
|
||||||
|
border: 1px solid #ff4757;
|
||||||
|
color: #ff4757;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor Section */
|
||||||
|
.editor-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #e4e4e4;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#save-extlinux:hover {
|
||||||
|
background: #00ff88;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
#clear-extlinux:hover {
|
||||||
|
background: #ff4757;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#extlinux-editor {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
color: #e4e4e4;
|
||||||
|
font-family: "Monaco", "Menlo", monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
#extlinux-editor:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #00d9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-status {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-status.success {
|
||||||
|
background: rgba(0, 255, 136, 0.2);
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-status.error {
|
||||||
|
background: rgba(255, 71, 87, 0.2);
|
||||||
|
color: #ff4757;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reboot Section */
|
||||||
|
.reboot-section {
|
||||||
|
border: 1px solid rgba(255, 71, 87, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reboot-section h2 {
|
||||||
|
color: #ff4757;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reboot-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reboot-warning {
|
||||||
|
color: #ff4757;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(255, 71, 87, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reboot-btn {
|
||||||
|
padding: 15px 40px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(90deg, #ff4757, #ff6b7a);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reboot-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(255, 71, 87, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reboot-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reboot-message {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reboot-message.success {
|
||||||
|
display: block;
|
||||||
|
background: rgba(0, 255, 136, 0.2);
|
||||||
|
border: 1px solid #00ff88;
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reboot-message.error {
|
||||||
|
display: block;
|
||||||
|
background: rgba(255, 71, 87, 0.2);
|
||||||
|
border: 1px solid #ff4757;
|
||||||
|
color: #ff4757;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
header h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tabs {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user