Are you sure you want to delete the post ?
35 |├── .gitignore ├── LICENSE ├── README.md ├── firmware ├── v2 │ └── web-server │ │ ├── api.ino │ │ ├── internal.ino │ │ ├── kvdb.ino │ │ ├── router.ino │ │ ├── server.ino │ │ ├── upload.ino │ │ ├── utils.ino │ │ └── web-server.ino ├── v3 │ └── web-server │ │ ├── api.ino │ │ ├── internal.ino │ │ ├── kvdb.ino │ │ ├── lib │ │ └── WakeOnLan.zip │ │ ├── router.ino │ │ ├── server.ino │ │ ├── share.ino │ │ ├── upload.ino │ │ ├── users.ino │ │ ├── utils.ino │ │ ├── web-server.ino │ │ ├── web-server.ino.d1_mini.bin │ │ └── web-server.ino.d1_mini.zip └── v4 │ └── web-server │ ├── api.ino │ ├── internal.ino │ ├── kvdb.ino │ ├── lib │ └── WakeOnLan.zip │ ├── router.ino │ ├── server.ino │ ├── share.ino │ ├── upload.ino │ ├── users.ino │ ├── utils.ino │ ├── web-server.ino │ ├── web-server.ino.d1_mini.bin │ └── web-server.ino.d1_mini.zip ├── img └── README │ ├── 1.jpg │ ├── image-20230814160329296.png │ ├── image-20230814160331808.png │ ├── image-20230814160505939.png │ ├── image-20230814160534701.png │ ├── image-20230814160904725.png │ ├── image-20230814161008552.png │ ├── image-20230814161027242.png │ ├── image-20230814161040075.png │ ├── image-20230814161104181.png │ ├── image-20240323120647781.png │ ├── image-20240323120841577.png │ ├── image-20240323120851503.png │ ├── image-20240323120859631.png │ ├── image-20240323120923170.png │ ├── image-20240323120934323.png │ ├── image-20240323121044080.png │ ├── image-20240323122057879.png │ ├── image-20250510203307491.png │ ├── image-20250510203347375.png │ ├── image-20250510203411915.png │ ├── image-20250510203428665.png │ ├── image-20250510203529903.png │ ├── image-20250510203605617.png │ ├── image-20250510203636902.png │ └── image-20250510203736747.png ├── pcb ├── v2 │ ├── BOM_webstick v2_2023-08-18.csv │ ├── Gerber_webstick v2.1.zip │ ├── Gerber_webstick v2.zip │ ├── PCB_webstick v2_2023-11-09.json │ └── PickAndPlace_webstick v2_2023-08-18.csv ├── v3 │ ├── BOM_Website-Stick_2024-03-20.csv │ ├── Gerber_Website-Stick_webstick-v3_2024-03-20.zip │ ├── PCB_webstick-v3_2024-03-20.json │ └── PickAndPlace_webstick-v3_2024-03-20.csv └── v4 │ ├── BOM_Website-Stick_2025-02-28.csv │ ├── Gerber_Website-Stick_webstick-v4_2025-02-28.zip │ ├── PCB_webstick-v4_2025-02-28.json │ └── PickAndPlace_webstick-v4_2025-02-28.csv ├── res ├── favicon.png ├── favicon.psd ├── logo.png └── logo.psd └── sd_card ├── README.md ├── cfg ├── admin.txt ├── mdns.txt └── wifi.txt └── www ├── about.html ├── admin ├── fs.css ├── fs.html ├── img │ ├── arrow-left.svg │ ├── arrow-right.svg │ ├── cluster.svg │ ├── eq.svg │ ├── file.svg │ ├── folder.svg │ ├── icon.svg │ ├── network.svg │ ├── opr │ │ ├── copy.svg │ │ ├── delete.svg │ │ ├── download.svg │ │ ├── new_folder.svg │ │ ├── open.svg │ │ ├── paste.svg │ │ ├── share.svg │ │ └── upload.svg │ ├── post-engine.svg │ ├── user.svg │ ├── users.svg │ ├── wifi.svg │ ├── zoom-in.svg │ └── zoom-out.svg ├── index.html ├── info.html ├── mde │ ├── index.html │ └── script │ │ ├── mde.css │ │ └── mde.js ├── music.html ├── notepad │ ├── ace │ │ └── editor.html │ └── index.html ├── photo.html ├── post.html ├── posteng │ ├── all.html │ ├── edit.html │ ├── library.html │ ├── new.html │ ├── newpg.html │ ├── pages.html │ ├── paste.html │ ├── pedit.html │ ├── setting.html │ └── src │ │ └── en.js ├── script │ ├── DPlayer.min.js │ └── DPlayer.min.js.map ├── search.html ├── share.html ├── shares.html ├── upld.js ├── users.html └── video.html ├── down ├── Rise Above.mp3 ├── minecraft.webm ├── photo.jpg └── text.txt ├── favicon.png ├── img ├── logo.png ├── selfie.jpg └── wallpaper.jpg ├── index.html ├── login.html ├── posts.html ├── qr.html ├── site ├── img │ └── index.html ├── index.html ├── pages │ └── index.html └── posts │ ├── 1710676940_Hello World!.md │ └── index.html └── store └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> C 2 | # Object files 3 | *.o 4 | *.ko 5 | *.obj 6 | *.elf 7 | 8 | # Precompiled Headers 9 | *.gch 10 | *.pch 11 | 12 | # Libraries 13 | *.lib 14 | *.a 15 | *.la 16 | *.lo 17 | 18 | # Shared objects (inc. Windows DLLs) 19 | *.dll 20 | *.so 21 | *.so.* 22 | *.dylib 23 | 24 | # Executables 25 | *.exe 26 | *.out 27 | *.app 28 | *.i*86 29 | *.x86_64 30 | *.hex 31 | 32 | # Debug files 33 | *.dSYM/ 34 | 35 | autopush.sh 36 | 37 | -------------------------------------------------------------------------------- /firmware/v2/web-server/kvdb.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Key Value database 4 | 5 | This is a file system based database 6 | that uses foldername as table name, 7 | filename as key and content as value 8 | 9 | Folder name and filename are limited to 10 | 5 characters as SDFS requirements. 11 | */ 12 | 13 | //Root of the db on SD card, **must have tailing slash** 14 | const String DB_root = "/db/"; 15 | 16 | //Clean the input for any input string 17 | String DBCleanInput(const String& inputString) { 18 | String trimmedString = inputString; 19 | //Replae all the slash that might breaks the file system 20 | trimmedString.replace("/", ""); 21 | //Trim off the space before and after the string 22 | trimmedString.trim(); 23 | return trimmedString; 24 | } 25 | 26 | //Database init create all the required table for basic system operations 27 | void DBInit() { 28 | DBNewTable("auth"); 29 | DBNewTable("pref"); 30 | } 31 | 32 | //Create a new Database table 33 | void DBNewTable(String tableName) { 34 | tableName = DBCleanInput(tableName); 35 | if (!SD.exists(DB_root + tableName)) { 36 | SD.mkdir(DB_root + tableName); 37 | } 38 | } 39 | 40 | 41 | //Check if a database table exists 42 | bool DBTableExists(String tableName) { 43 | tableName = DBCleanInput(tableName); 44 | return SD.exists(DB_root + tableName); 45 | } 46 | 47 | //Write a key to a table, return true if succ 48 | bool DBWrite(String tableName, String key, String value) { 49 | if (!DBTableExists(tableName)) { 50 | return false; 51 | } 52 | tableName = DBCleanInput(tableName); 53 | key = DBCleanInput(key); 54 | String fsDataPath = DB_root + tableName + "/" + key; 55 | if (SD.exists(fsDataPath)) { 56 | //Entry already exists. Delete it 57 | SD.remove(fsDataPath); 58 | } 59 | 60 | //Write new data to it 61 | File targetEntry = SD.open(fsDataPath, FILE_WRITE); 62 | targetEntry.print(value); 63 | targetEntry.close(); 64 | return true; 65 | } 66 | 67 | //Read from database, require table name and key 68 | String DBRead(String tableName, String key) { 69 | if (!DBTableExists(tableName)) { 70 | return ""; 71 | } 72 | tableName = DBCleanInput(tableName); 73 | key = DBCleanInput(key); 74 | String fsDataPath = DB_root + tableName + "/" + key; 75 | if (!SD.exists(fsDataPath)) { 76 | //Target not exists. Return empty string 77 | return ""; 78 | } 79 | 80 | String value = ""; 81 | File targetEntry = SD.open(fsDataPath, FILE_READ); 82 | while (targetEntry.available()) { 83 | value = value + targetEntry.readString(); 84 | } 85 | 86 | targetEntry.close(); 87 | return value; 88 | } 89 | 90 | //Check if a given key exists in the database 91 | bool DBKeyExists(String tableName, String key) { 92 | if (!DBTableExists(tableName)) { 93 | return false; 94 | } 95 | tableName = DBCleanInput(tableName); 96 | key = DBCleanInput(key); 97 | String fsDataPath = DB_root + tableName + "/" + key; 98 | return SD.exists(fsDataPath); 99 | } 100 | 101 | //Remove the key-value item from db, return true if succ 102 | bool DBRemove(String tableName, String key) { 103 | if (!DBKeyExists(tableName, key)){ 104 | return false; 105 | } 106 | tableName = DBCleanInput(tableName); 107 | key = DBCleanInput(key); 108 | String fsDataPath = DB_root + tableName + "/" + key; 109 | SD.remove(fsDataPath); 110 | return true; 111 | } 112 | -------------------------------------------------------------------------------- /firmware/v2/web-server/router.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Router.ino 4 | 5 | This is the main router of the whole web server. 6 | It is like the apache.conf where you handle routing 7 | to different services. 8 | 9 | By default, all route will go to the SD card /www/ folder 10 | */ 11 | class MainRouter : public AsyncWebHandler { 12 | public: 13 | MainRouter() {} 14 | virtual ~MainRouter() {} 15 | 16 | bool canHandle(AsyncWebServerRequest *request) { 17 | String requestURI = request->url().c_str(); 18 | if (requestURI.equals("/upload")) { 19 | //File Upload Endpoint 20 | return false; 21 | } else if (requestURI.startsWith("/api/")) { 22 | //API paths 23 | return false; 24 | } 25 | return true; 26 | } 27 | 28 | //Main Routing Logic Here 29 | void handleRequest(AsyncWebServerRequest *request) { 30 | String requestURI = request->url().c_str(); 31 | 32 | /* Rewrite the request path if URI contains ./ */ 33 | if (requestURI.indexOf("./") > 0) { 34 | requestURI.replace("./", ""); 35 | AsyncWebServerResponse *response = request->beginResponse(307); 36 | response->addHeader("Cache-Control", "no-cache"); 37 | response->addHeader("Location", requestURI); 38 | request->send(response); 39 | return; 40 | } 41 | 42 | /* Special Routing Rules */ 43 | //Redirect / back to index.html 44 | if (requestURI == "/") { 45 | request->redirect("/index.html"); 46 | return; 47 | } 48 | 49 | //Special interfaces that require access controls 50 | if (requestURI.startsWith("/store/")) { 51 | //Private file storage. Not allow access 52 | AsyncWebServerResponse *response = request->beginResponse(401, "text/html", "403 - Forbidden"); 53 | request->send(response); 54 | return; 55 | } 56 | 57 | /* Default Routing Rules */ 58 | 59 | Serial.println("URI: " + requestURI + " | MIME: " + getMime(requestURI)); 60 | //Check if the file exists on the SD card 61 | if (SD.exists("/www" + requestURI)) { 62 | // File exists on SD card web root 63 | if (IsDir("/www" + requestURI)) { 64 | //Requesting a directory 65 | if (!requestURI.endsWith("/")) { 66 | //Missing tailing slash 67 | request->redirect(requestURI + "/"); 68 | return; 69 | } 70 | 71 | if (SD.exists("/www" + requestURI + "index.html")) { 72 | request->send(SDFS, "/www" + requestURI + "/index.html", "text/html", false); 73 | } else { 74 | HandleDirRender(request, requestURI , "/www" + requestURI); 75 | } 76 | 77 | } else { 78 | request->send(SDFS, "/www" + requestURI, getMime(requestURI), false); 79 | } 80 | } else { 81 | // File does not exist in web root 82 | AsyncResponseStream *response = request->beginResponseStream("text/html"); 83 | Serial.println("NOT FOUND: " + requestURI); 84 | prettyPrintRequest(request); 85 | response->print("
404 - Not Found
"); 87 | response->printf("Requesting http://%s with URI: %s
", request->host().c_str(), request->url().c_str()); 88 | response->print(""); 89 | request->send(response); 90 | } 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /firmware/v2/web-server/server.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Web Server 4 | 5 | This is the main entry point of the WebStick bare metal 6 | web server. If you have exception rules that shall not 7 | be handled by the main router, you can do them here. 8 | 9 | */ 10 | 11 | //Check if a user is authenticated / logged in 12 | bool IsUserAuthed(AsyncWebServerRequest *request) { 13 | if (request->hasHeader("Cookie")) { 14 | //User cookie from browser 15 | String authCookie = GetCookieValueByKey(request, "web-auth"); 16 | if (authCookie == "") { 17 | return false; 18 | } 19 | 20 | //Match it to the server side value in kvdb 21 | //Serial.println(authCookie); //user cookie 22 | //Serial.println(authSession); //server session 23 | if (authSession == "") { 24 | //Server side has no resumable login session 25 | return false; 26 | } 27 | if (authCookie.equals(authSession)) { 28 | return true; 29 | } 30 | return false; 31 | } else { 32 | Serial.println("Cookie Missing"); 33 | return false; 34 | } 35 | } 36 | 37 | //Reply the request by a directory list 38 | void HandleDirRender(AsyncWebServerRequest *r, String dirName, String dirToList) { 39 | AsyncResponseStream *response = r->beginResponseStream("text/html"); 40 | //Serve directory entries 41 | File directory = SD.open(dirToList); 42 | 43 | // Check if the directory is open 44 | if (!directory) { 45 | SendErrorResp(r, "unable to open directory"); 46 | return; 47 | } 48 | 49 | response->print("404 - Not Found
"); 90 | response->printf("Requesting http://%s with URI: %s
", request->host().c_str(), request->url().c_str()); 91 | response->print(""); 92 | request->send(response); 93 | } 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /firmware/v3/web-server/server.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Web Server 4 | 5 | This is the main entry point of the WebStick bare metal 6 | web server. If you have exception rules that shall not 7 | be handled by the main router, you can do them here. 8 | 9 | */ 10 | 11 | //Check if a user is authenticated / logged in 12 | bool IsUserAuthed(AsyncWebServerRequest *request) { 13 | if (request->hasHeader("Cookie")) { 14 | //User cookie from browser 15 | String authCookie = GetCookieValueByKey(request, "web-auth"); 16 | if (authCookie == "") { 17 | return false; 18 | } 19 | 20 | //Match it to the server side value in kvdb 21 | if (authSession == "") { 22 | //Server side has no resumable login session 23 | return false; 24 | } 25 | if (authCookie.equals(authSession)) { 26 | //Admin login 27 | return true; 28 | }else if (DBKeyExists("sess", authCookie)){ 29 | //User login 30 | return true; 31 | } 32 | 33 | return false; 34 | } else { 35 | Serial.println("Cookie Missing"); 36 | return false; 37 | } 38 | } 39 | 40 | //Check if a user is authenticated and is Admin 41 | bool IsAdmin(AsyncWebServerRequest *request) { 42 | if (request->hasHeader("Cookie")) { 43 | //User cookie from browser 44 | String authCookie = GetCookieValueByKey(request, "web-auth"); 45 | if (authCookie == "") { 46 | return false; 47 | } 48 | 49 | //Match it to the server side value in kvdb 50 | if (authSession == "") { 51 | //Server side has no resumable login session 52 | return false; 53 | } 54 | if (authCookie.equals(authSession)) { 55 | return true; 56 | } 57 | return false; 58 | } else { 59 | return false; 60 | } 61 | } 62 | 63 | 64 | //Reply the request by a directory list 65 | void HandleDirRender(AsyncWebServerRequest *r, String dirName, String dirToList) { 66 | AsyncResponseStream *response = r->beginResponseStream("text/html"); 67 | //Serve directory entries 68 | File directory = SD.open(dirToList); 69 | 70 | // Check if the directory is open 71 | if (!directory) { 72 | SendErrorResp(r, "unable to open directory"); 73 | return; 74 | } 75 | 76 | response->print("404 - Not Found
"); 89 | response->printf("Requesting http://%s with URI: %s
", request->host().c_str(), request->url().c_str()); 90 | response->print(""); 91 | request->send(response); 92 | } 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /firmware/v4/web-server/server.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Web Server 4 | 5 | This is the main entry point of the WebStick bare metal 6 | web server. If you have exception rules that shall not 7 | be handled by the main router, you can do them here. 8 | 9 | */ 10 | 11 | //Check if a user is authenticated / logged in 12 | bool IsUserAuthed(AsyncWebServerRequest *request) { 13 | if (request->hasHeader("Cookie")) { 14 | //User cookie from browser 15 | String authCookie = GetCookieValueByKey(request, "web-auth"); 16 | if (authCookie == "") { 17 | return false; 18 | } 19 | 20 | //Check if it is user login (no state keeping) 21 | bool isUserLogin = DBKeyExists("sess", authCookie); 22 | if (isUserLogin){ 23 | //User login 24 | return true; 25 | } 26 | 27 | //Check if it is admin login (state keeping) 28 | if (authSession == "") { 29 | //Server side has no resumable login session 30 | return false; 31 | } 32 | 33 | bool isAdminLogin = authCookie.equals(authSession); 34 | if (isAdminLogin) { 35 | //Admin login 36 | return true; 37 | } 38 | 39 | return false; 40 | } else { 41 | Serial.println("Cookie Missing"); 42 | return false; 43 | } 44 | } 45 | 46 | //Check if a user is authenticated and is Admin 47 | bool IsAdmin(AsyncWebServerRequest *request) { 48 | if (request->hasHeader("Cookie")) { 49 | //User cookie from browser 50 | String authCookie = GetCookieValueByKey(request, "web-auth"); 51 | if (authCookie == "") { 52 | return false; 53 | } 54 | 55 | //Match it to the server side value in kvdb 56 | if (authSession == "") { 57 | //Server side has no resumable login session 58 | return false; 59 | } 60 | if (authCookie.equals(authSession)) { 61 | return true; 62 | } 63 | return false; 64 | } else { 65 | return false; 66 | } 67 | } 68 | 69 | 70 | //Reply the request by a directory list 71 | void HandleDirRender(AsyncWebServerRequest *r, String dirName, String dirToList) { 72 | AsyncResponseStream *response = r->beginResponseStream("text/html"); 73 | //Serve directory entries 74 | File directory = SD.open(dirToList); 75 | 76 | // Check if the directory is open 77 | if (!directory) { 78 | SendErrorResp(r, "unable to open directory"); 79 | return; 80 | } 81 | 82 | response->print("Item | 26 |Properties | 27 |
---|---|
Power Requirement | 31 |5V 1A | 32 |
Actual Power Draw | 35 |around 2 - 3W | 36 |
Wireless Network Speed | 39 |2 - 4 Mbps | 40 |
Max SD card size | 43 |4GB | 44 |
SD card format | 47 |FAT32 | 48 |
Arduino IDE Support | 51 |52 | |
Run PHP / nodejs | 55 |56 | |
Max No. of Users | 59 |~20 viewers per day | 60 |
Post Name | 9 |Creation Time | 10 |Edit | 11 |Delete | 12 |
---|---|---|---|
17 | 18 | | 19 |
Are you sure you want to delete the post ?
35 |The table below shows the screenshots pasted into the editor
5 |Image Name | 9 |Upload Time | 10 |Preview | 11 |Delete | 12 |
---|---|---|---|
17 | 18 | Loading... 19 | | 20 |
Click on the preview button to see the image
30 |Page Name | 9 |Edit | 10 |Delete | 11 ||
---|---|---|---|
16 | 17 | | 18 |
Are you sure you want to delete the page ?
34 |No pasted image
15 | 18 |File Size | 71 |72 | |
Share ID | 75 |76 | |
Download on another device
86 |31 | List of shares on this WebStick 32 | | 33 ||||
---|---|---|---|
38 | Loading 39 | | 40 |
Some share links might point to files that no longer exists on the SD card.
47 | Click the "Clean Shares" button below to remove broken share links and regenerate the shared link table.
This site is hosted on a WebStick designed by tobychui
79 |Redirecitng to homepage
8 | 9 | -------------------------------------------------------------------------------- /sd_card/www/site/pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |Redirecitng to homepage
8 | 9 | -------------------------------------------------------------------------------- /sd_card/www/site/posts/1710676940_Hello World!.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | Welcome to my web page. Fun facts: This is hosted on an ESP8266! 3 | 4 | ## WebSticks 5 | The WebStick is an Arduino programmed, mini web server stick power in a USB drive form factor. 6 | 7 | You might be wonder how it is possible to work right? Well, it is not simple. The make it work, I first 8 | need to write a web server in C++ and make it serve html files from an SD card formated as FAT. 9 | Next, I need to handle all kind of headers and werid things to make it happens. 10 | -------------------------------------------------------------------------------- /sd_card/www/site/posts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |Redirecitng to homepage
8 | 9 | -------------------------------------------------------------------------------- /sd_card/www/store/README.md: -------------------------------------------------------------------------------- 1 | # Store 2 | 3 | The store folder is a private storage folder which only allow admin access. You can upload files to this folder and use it as a tiny cloud storage. Due to the limitation on hardware, we do not recommend uploading file >5MB as this will take decades to upload and stream. 4 | --------------------------------------------------------------------------------