diff --git a/unity_package/Runtime/VELConnectManager.cs b/unity_package/Runtime/VELConnectManager.cs index f6027ac..c20a991 100644 --- a/unity_package/Runtime/VELConnectManager.cs +++ b/unity_package/Runtime/VELConnectManager.cs @@ -2,8 +2,10 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Reflection; using System.Text; +using System.Threading.Tasks; using Newtonsoft.Json; using UnityEngine; using UnityEngine.Networking; @@ -14,7 +16,8 @@ namespace VELConnect public class VELConnectManager : MonoBehaviour { public string velConnectUrl = "http://localhost"; - private static VELConnectManager instance; + public static string VelConnectUrl => _instance.velConnectUrl; + private static VELConnectManager _instance; public class State { @@ -27,6 +30,7 @@ namespace VELConnect public string last_modified; public Dictionary data; } + public class Device { public string hw_id; @@ -136,8 +140,8 @@ namespace VELConnect private void Awake() { - if (instance != null) Debug.LogError("VELConnectManager instance already exists", this); - instance = this; + if (_instance != null) Debug.LogError("VELConnectManager instance already exists", this); + _instance = this; } // Start is called before the first frame update @@ -382,10 +386,10 @@ namespace VELConnect if (sendInitialState) { - if (instance != null && instance.lastState?.device != null) + if (_instance != null && _instance.lastState?.device != null) { - if (instance.lastState.device.GetType().GetField(key) - ?.GetValue(instance.lastState.device) is string val) + if (_instance.lastState.device.GetType().GetField(key) + ?.GetValue(_instance.lastState.device) is string val) { try { @@ -472,12 +476,12 @@ namespace VELConnect public static string GetDeviceData(string key) { - return instance != null ? instance.lastState?.device?.TryGetData(key) : null; + return _instance != null ? _instance.lastState?.device?.TryGetData(key) : null; } public static string GetRoomData(string key) { - return instance != null ? instance.lastState?.room?.TryGetData(key) : null; + return _instance != null ? _instance.lastState?.room?.TryGetData(key) : null; } @@ -486,8 +490,8 @@ namespace VELConnect /// public static void SetDeviceField(Dictionary device) { - instance.PostRequestCallback( - instance.velConnectUrl + "/api/device/set_data/" + DeviceId, + PostRequestCallback( + _instance.velConnectUrl + "/api/device/set_data/" + DeviceId, JsonConvert.SerializeObject(device), new Dictionary { { "modified_by", DeviceId } } ); @@ -498,8 +502,8 @@ namespace VELConnect /// public static void SetDeviceData(Dictionary data) { - instance.PostRequestCallback( - instance.velConnectUrl + "/api/device/set_data/" + DeviceId, + PostRequestCallback( + _instance.velConnectUrl + "/api/device/set_data/" + DeviceId, JsonConvert.SerializeObject(new Dictionary { { "data", data } }), new Dictionary { { "modified_by", DeviceId } } ); @@ -513,18 +517,34 @@ namespace VELConnect return; } - instance.PostRequestCallback( - instance.velConnectUrl + "/api/set_data/" + Application.productName + "_" + VelNetManager.Room, + PostRequestCallback( + _instance.velConnectUrl + "/api/set_data/" + Application.productName + "_" + VelNetManager.Room, JsonConvert.SerializeObject(data), new Dictionary { { "modified_by", DeviceId } } ); } + public static void UploadFile(string fileName, byte[] fileData, Action successCallback = null) + { + MultipartFormDataContent requestContent = new MultipartFormDataContent(); + ByteArrayContent fileContent = new ByteArrayContent(fileData); - public void GetRequestCallback(string url, Action successCallback = null, + requestContent.Add(fileContent, "file", fileName); + + Task.Run(async () => + { + HttpResponseMessage r = await new HttpClient().PostAsync(_instance.velConnectUrl + "/api/upload_file", requestContent); + string resp = await r.Content.ReadAsStringAsync(); + Dictionary dict = JsonConvert.DeserializeObject>(resp); + successCallback?.Invoke(dict["key"]); + }); + } + + + public static void GetRequestCallback(string url, Action successCallback = null, Action failureCallback = null) { - StartCoroutine(GetRequestCallbackCo(url, successCallback, failureCallback)); + _instance.StartCoroutine(_instance.GetRequestCallbackCo(url, successCallback, failureCallback)); } private IEnumerator GetRequestCallbackCo(string url, Action successCallback = null, @@ -548,13 +568,14 @@ namespace VELConnect } } - public void PostRequestCallback(string url, string postData, Dictionary headers = null, + public static void PostRequestCallback(string url, string postData, Dictionary headers = null, Action successCallback = null, Action failureCallback = null) { - StartCoroutine(PostRequestCallbackCo(url, postData, headers, successCallback, failureCallback)); + _instance.StartCoroutine(PostRequestCallbackCo(url, postData, headers, successCallback, failureCallback)); } + private static IEnumerator PostRequestCallbackCo(string url, string postData, Dictionary headers = null, Action successCallback = null, Action failureCallback = null) diff --git a/velconnect/CreateDB.sql b/velconnect/CreateDB.sql index ebc88b8..380017c 100644 --- a/velconnect/CreateDB.sql +++ b/velconnect/CreateDB.sql @@ -72,7 +72,7 @@ CREATE TABLE `DataBlock` ( `id` TEXT NOT NULL, -- id of the owner of this file. Ownership is not transferable because ids may collide, -- but the owner could be null for global scope - `owner_id` TEXT, + `owner_id` TEXT NOT NULL DEFAULT 'none', `visibility` TEXT CHECK( `visibility` IN ('public','private','unlisted') ) NOT NULL DEFAULT 'public', -- This is an indexable field to filter out different types of datablocks `category` TEXT, diff --git a/velconnect/routes/api.py b/velconnect/routes/api.py index 3dbf467..4096c6e 100644 --- a/velconnect/routes/api.py +++ b/velconnect/routes/api.py @@ -1,3 +1,4 @@ +import os import secrets import json import string @@ -129,7 +130,7 @@ def create_device(hw_id: str): @router.get('/device/get_data/{hw_id}') -def get_state(request: Request, response: Response, hw_id: str): +def get_device_data(request: Request, response: Response, hw_id: str): """Gets the device state""" devices = db.query(""" @@ -155,7 +156,7 @@ def get_state(request: Request, response: Response, hw_id: str): @router.post('/device/set_data/{hw_id}') -def set_state(request: fastapi.Request, hw_id: str, data: dict, modified_by: str = None): +def set_device_data(request: fastapi.Request, hw_id: str, data: dict, modified_by: str = None): """Sets the device state""" create_device(hw_id) @@ -343,21 +344,53 @@ def get_user_dict(user_id: str) -> dict | None: return None +@router.post("/upload_file") +async def upload_file_with_random_key(request: fastapi.Request, file: UploadFile, modified_by: str = None): + return await upload_file(request, file, None, modified_by) + + @router.post("/upload_file/{key}") -async def upload_file(request: fastapi.Request, file: UploadFile, key: str, modified_by: str = None): +async def upload_file(request: fastapi.Request, file: UploadFile, key: str | None, modified_by: str = None): + if not os.path.exists('data'): + os.makedirs('data') + + # generates a key if none was supplied + if key is None: + key = generate_id() + + # regenerate if necessary + while len(db.query("SELECT id FROM `DataBlock` WHERE id=:id;", {"id": key})) > 0: + key = generate_id() + async with aiofiles.open('data/' + key, 'wb') as out_file: content = await file.read() # async read await out_file.write(content) # async write # add a datablock to link to the file - set_data(request, {'filename': file.filename}, key, 'file', modified_by) - return {"filename": file.filename} + set_data(request, data={'filename': file.filename}, key=key, category='file', modified_by=modified_by) + return {"filename": file.filename, 'key': key} @router.get("/download_file/{key}") -async def download_file(key: str): +async def download_file(response: Response, key: str): # get the relevant datablock - data = get_data(key) + data = get_data(response, key) print(data) + if response.status_code == status.HTTP_404_NOT_FOUND: + return 'Not found' if data['category'] != 'file': - return 'Not a file', 500 - return fastapi.FileResponse(data['data']['filename']) + response.status_code = status.HTTP_400_BAD_REQUEST + return 'Not a file' + return FileResponse(path='data/' + key, filename=data['data']['filename']) + + +@router.get("/get_all_files") +async def get_all_files(): + data = db.query(""" + SELECT * + FROM `DataBlock` + WHERE visibility='public' AND category='file'; + """) + data = [dict(f) for f in data] + for f in data: + parse_data(f) + return data