From ae147771cba543f6411fb37a95c5aa4a94f6c4e9 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Fri, 23 May 2025 12:28:52 -0600 Subject: [PATCH 1/4] Start working on the server --- .../ridgescout/ui/transfer/FTPSync.java | 2 +- server/README.md | 0 server/main.py | 42 +++++++++++++++++++ server/requirements.txt | 1 + server/utils.py | 30 +++++++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 server/README.md create mode 100644 server/main.py create mode 100644 server/requirements.txt create mode 100644 server/utils.py diff --git a/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/FTPSync.java b/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/FTPSync.java index 10d2aae..4c05be6 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/FTPSync.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/FTPSync.java @@ -26,7 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -// Class to synchronise data over FTP. +// This is now deprsicated public class FTPSync extends Thread { public static final String remoteBasePath = "/RidgeScout/"; public static final String timestampsFilename = "timestamps"; diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..e69de29 diff --git a/server/main.py b/server/main.py new file mode 100644 index 0000000..1f2f24b --- /dev/null +++ b/server/main.py @@ -0,0 +1,42 @@ +import os +import json +from bottle import Bottle, run, get, static_file, response + +from utils import * + +app = Bottle() + +file_metadata = {} + +def save_metadata(): + global file_metadata + write(METADATA_FILE, json.dumps(file_metadata)) + +def load_metadata(): + global file_metadata + data = read(METADATA_FILE) + if data is not None: + file_metadata = json.loads(data) + +# @app.route('/') +# def list(): +# response.content_type = 'application/json' +# return json.dumps(ls(DATA_ROOT)) + + +@app.route('/api/metadata') +def metadata(): + global file_metadata + load_metadata() + response.content_type = 'application/json' + return json.dumps(file_metadata) + + + +# @app.route('/') +# def hello(filename): +# return static_file(DATA_ROOT, filename) + +if __name__ == '__main__': + mkdir(DATA_ROOT) + app.run(host='localhost', port=8080) diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..310dc0b --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1 @@ +bottle diff --git a/server/utils.py b/server/utils.py new file mode 100644 index 0000000..44c1b5a --- /dev/null +++ b/server/utils.py @@ -0,0 +1,30 @@ +import os + +ROOT = os.path.dirname(__file__) +METADATA_FILE = os.path.join(ROOT, 'metadata.json') +DATA_ROOT = os.path.join(os.path.dirname(__file__), 'data') + +def mkdir(path): + if not os.path.exists(path): + os.makedirs(path) + +def ls(path): + try: + return os.listdir(path) + except: + return [] + +def read(path): + if not os.path.exists(path): + return None + try: + with open(path) as f: + return f.read() + except: + return None + +def write(path, data): + if not os.path.exists(path): + with open(path, mode='a'): pass + with open(path, 'w') as f: + f.write(data) From 5d727cf35972e8fbadbd498bb73a8e22466c4ba1 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Sun, 25 May 2025 13:38:50 -0600 Subject: [PATCH 2/4] Add upload and download to python server --- server/main.py | 105 ++++++++++++++++++++++++++++++++++++---- server/requirements.txt | 1 + server/utils.py | 20 ++++++-- 3 files changed, 111 insertions(+), 15 deletions(-) diff --git a/server/main.py b/server/main.py index 1f2f24b..457abd5 100644 --- a/server/main.py +++ b/server/main.py @@ -1,6 +1,10 @@ +from ast import mod import os import json -from bottle import Bottle, run, get, static_file, response +import hashlib +from datetime import datetime +from bottle import Bottle, run, get, put, static_file, response, request,HTTPResponse +from random import SystemRandom from utils import * @@ -10,18 +14,52 @@ file_metadata = {} def save_metadata(): global file_metadata - write(METADATA_FILE, json.dumps(file_metadata)) + write(METADATA_PATH4, json.dumps(file_metadata)) def load_metadata(): global file_metadata - data = read(METADATA_FILE) + data = read(METADATA_PATH4) if data is not None: file_metadata = json.loads(data) -# @app.route('/') -# def list(): -# response.content_type = 'application/json' -# return json.dumps(ls(DATA_ROOT)) +api_key = None +cryptogen = SystemRandom() + +def aquire_key(): + global api_key + global cryptogen + + if api_key is None: + try: + api_key = read(API_KEY_PATH).decode("utf-8").strip() + except: + ran = cryptogen.randrange(10**80) + api_key = "%064x" % ran + write(API_KEY_PATH, api_key) + +@app.route('/') +def list_html(): + global file_metadata + load_metadata() + + content = '' + for heading in ['File', 'Size', 'Modified', 'Sha256']: + content += f'' + content += "" + + print(file_metadata) + + for filename in file_metadata.keys(): + content += "" + content += f'' + content += f'' + content += f'' + content += f'' + content += "" + + content += '
{heading}
{filename}{file_metadata[filename]["size"]}B{file_metadata[filename]["modified"]}{file_metadata[filename]["sha256"]}
' + + return content @app.route('/api/metadata') @@ -32,11 +70,58 @@ def metadata(): return json.dumps(file_metadata) +@app.route('/api/', method='PUT') +def upload(filename): + global api_key + try: + sentkey = request.headers[API_KEY_HEADER] + if sentkey != api_key: + return HTTPResponse(status=403, body=f"Invalid Key {sentkey}, {api_key}") + except: + return HTTPResponse(status=403, body="You must specify an 'api_key' header") -# @app.route('/') -# def hello(filename): -# return static_file(DATA_ROOT, filename) + + global file_metadata + load_metadata() + + data = request.body.read() + + try: + modified = request.headers[MODIFIED_HEADER] + except: + modified = (datetime.now() - datetime(1970, 1, 1)).total_seconds() + + sha256_hash = hashlib.sha256() + sha256_hash.update(data) + + file_metadata[filename] = { + 'size': len(data), + 'modified': modified, + 'sha256': sha256_hash.hexdigest() + } + + save_metadata() + + + + write(os.path.join(DATA_ROOT, filename), data) + # save_metadata() + # response.content_type = 'application/json' + # return json.dumps(file_metadata) + + +@app.route('/api/') +def download(filename): + + data = read(os.path.join(DATA_ROOT, filename)) + + if data is not None: + response.content_type = 'application/octet-stream' + return data + else: + HTTPResponse(status=404, body="File not found") if __name__ == '__main__': mkdir(DATA_ROOT) + aquire_key() app.run(host='localhost', port=8080) diff --git a/server/requirements.txt b/server/requirements.txt index 310dc0b..7851c1b 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1 +1,2 @@ bottle +random diff --git a/server/utils.py b/server/utils.py index 44c1b5a..48114b7 100644 --- a/server/utils.py +++ b/server/utils.py @@ -1,9 +1,14 @@ import os ROOT = os.path.dirname(__file__) -METADATA_FILE = os.path.join(ROOT, 'metadata.json') DATA_ROOT = os.path.join(os.path.dirname(__file__), 'data') +METADATA_PATH4 = os.path.join(ROOT, 'metadata.json') +API_KEY_PATH = os.path.join(ROOT, 'api_key.txt') + +MODIFIED_HEADER = 'modified' +API_KEY_HEADER = 'api_key' + def mkdir(path): if not os.path.exists(path): os.makedirs(path) @@ -18,13 +23,18 @@ def read(path): if not os.path.exists(path): return None try: - with open(path) as f: + with open(path, 'rb') as f: return f.read() - except: + except Exception as e: + print(f"Error reading file {path}: {e}") return None def write(path, data): if not os.path.exists(path): - with open(path, mode='a'): pass - with open(path, 'w') as f: + with open(path, mode='ab'): pass + + if isinstance(data, str): + data = str.encode(data) + + with open(path, 'wb') as f: f.write(data) From e278bc10a1617a9b5bd6bee7653632e99bd3c9b4 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Sun, 25 May 2025 18:48:02 -0600 Subject: [PATCH 3/4] Add java side of http syncing --- .gitignore | 6 + .../ui/settings/SettingsFragment.java | 9 +- .../ridgescout/ui/transfer/FTPSync.java | 3 +- .../ridgescout/ui/transfer/HttpSync.java | 369 ++++++++++++++++++ .../ui/transfer/TransferFragment.java | 10 +- .../ridgescout/utility/HttpGetFile.java | 168 ++++++++ .../ridgescout/utility/HttpPutFile.java | 161 ++++++++ .../ridgescout/utility/SettingsManager.java | 7 +- server/main.py | 4 +- server/utils.py | 2 +- 10 files changed, 726 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/HttpSync.java create mode 100644 app/src/main/java/com/ridgebotics/ridgescout/utility/HttpGetFile.java create mode 100644 app/src/main/java/com/ridgebotics/ridgescout/utility/HttpPutFile.java diff --git a/.gitignore b/.gitignore index f512cf7..24b0a61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +# Python server +__pycache__/ +metadata.json +api_key.txt +server_data/ + # Gradle files .gradle/ build/ diff --git a/app/src/main/java/com/ridgebotics/ridgescout/ui/settings/SettingsFragment.java b/app/src/main/java/com/ridgebotics/ridgescout/ui/settings/SettingsFragment.java index 2695244..7e47aca 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/ui/settings/SettingsFragment.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/ui/settings/SettingsFragment.java @@ -46,6 +46,7 @@ import com.ridgebotics.ridgescout.ui.views.CustomSpinnerView; import com.ridgebotics.ridgescout.ui.views.TallyCounterView; import com.ridgebotics.ridgescout.utility.DataManager; import com.ridgebotics.ridgescout.utility.FileEditor; +import com.ridgebotics.ridgescout.utility.SettingsManager; import java.util.ArrayList; import java.util.Arrays; @@ -92,11 +93,13 @@ public class SettingsFragment extends Fragment { manager.addItem(new CheckboxSettingsItem(CustomEventsKey, "Custom Events")); - StringSettingsItem FTPServer = new StringSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPServer, "FTP Server (Sync)"); + StringSettingsItem FTPKey = new StringSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPKey, "Sync Key"); + manager.addItem(FTPKey); + StringSettingsItem FTPServer = new StringSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPServer, "Sync Server (Sync)"); manager.addItem(FTPServer); - CheckboxSettingsItem FTPSendMetaFiles = new CheckboxSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPSendMetaFiles, "Sync meta files"); + CheckboxSettingsItem FTPSendMetaFiles = new CheckboxSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPSendMetaFiles, "⚠ Send meta files"); manager.addItem(FTPSendMetaFiles); - CheckboxSettingsItem FTPEnabled = new CheckboxSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPEnabled, "FTP Enabled", FTPServer, FTPSendMetaFiles); + CheckboxSettingsItem FTPEnabled = new CheckboxSettingsItem(com.ridgebotics.ridgescout.utility.SettingsManager.FTPEnabled, "FTP Enabled", FTPServer, FTPKey, FTPSendMetaFiles); manager.addItem(FTPEnabled); manager.addItem(new CheckboxSettingsItem(WifiModeKey, "Wifi Mode", FTPEnabled)); diff --git a/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/FTPSync.java b/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/FTPSync.java index 4c05be6..1cefd16 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/FTPSync.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/FTPSync.java @@ -26,7 +26,8 @@ import java.util.List; import java.util.Map; import java.util.Set; -// This is now deprsicated +// This is now deprecated +// Class to synchronise data over FTP. public class FTPSync extends Thread { public static final String remoteBasePath = "/RidgeScout/"; public static final String timestampsFilename = "timestamps"; diff --git a/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/HttpSync.java b/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/HttpSync.java new file mode 100644 index 0000000..53d5bc7 --- /dev/null +++ b/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/HttpSync.java @@ -0,0 +1,369 @@ +package com.ridgebotics.ridgescout.ui.transfer; + +import static com.ridgebotics.ridgescout.utility.FileEditor.baseDir; + +import com.ridgebotics.ridgescout.utility.AlertManager; +import com.ridgebotics.ridgescout.utility.BuiltByteParser; +import com.ridgebotics.ridgescout.utility.ByteBuilder; +import com.ridgebotics.ridgescout.utility.FileEditor; +import com.ridgebotics.ridgescout.utility.HttpGetFile; +import com.ridgebotics.ridgescout.utility.HttpPutFile; +import com.ridgebotics.ridgescout.utility.RequestTask; +import com.ridgebotics.ridgescout.utility.SettingsManager; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +// This is now deprsicated +// Class to syncronise data over FTP. +public class HttpSync extends Thread { + public static final String timestampsFilename = "timestamps"; + + private static final long millisTolerance = 1000; + + private boolean after(Date a, Date b){ + return a.getTime() - b.getTime() > millisTolerance; + } + + + public interface onResult { + void onResult(boolean error, int upCount, int downCount); + } + public interface UpdateIndicator { + void onText(String text); + } + private static UpdateIndicator updateIndicator = text -> {}; + public static String text = ""; + private static void setUpdateIndicator(String m_text){ + text = m_text; + updateIndicator.onText(m_text); + } + public static void setOnUpdateIndicator(UpdateIndicator m_updateIndicator){ + updateIndicator = m_updateIndicator; + } + + private static onResult onResult = (error, upCount, downCount) -> {}; + public static void setOnResult(onResult result){ + onResult = result; + } + + private static boolean isRunning = false; + public static boolean getIsRunning(){return isRunning;} + + public static void sync(){ +// DataManager.reload_event(); + HttpSync sync = new HttpSync(); + + sync.start(); + } + + private int upCount = 0; + private int downCount = 0; + + private class TransferFile { + public String filename; + public Date updated; + public String checksum; + } + + private List localFiles = new ArrayList<>(); + private List remoteFiles = new ArrayList<>(); + + + + private void await() { + while(!runningRequest.get()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) {} + } + } + + + AtomicBoolean runningRequest = new AtomicBoolean(false); + + public void run() { + isRunning = true; + boolean sendMetaFiles = SettingsManager.getFTPSendMetaFiles(); + String serverIP = SettingsManager.getFTPServer(); + String serverKey = SettingsManager.getFTPKey(); + + setUpdateIndicator("Getting Metadata..."); + + // Load metadata from server + getRemoteFileMetadata(serverIP, serverKey); + + if(!isRunning){ + setUpdateIndicator("Error Connecting"); + onResult.onResult(true, upCount, downCount); + return; + } + + + getLocalFileMetadata(); + + + + + // Wait for metadata request to finish + + setUpdateIndicator("Uploading 0%"); + + for(int i = 0; i < localFiles.size(); i++){ + TransferFile localFile = localFiles.get(i); + + System.out.print("LocalFile: " + localFile.filename + ", " + localFile.checksum + ", " + localFile.updated + ": "); + TransferFile remoteFile = findInFileArray(remoteFiles, localFile.filename); + + + + if( + ( + sendMetaFiles || !( + localFile.filename.endsWith(".fields") + ) + + ) + && (remoteFile == null || + ( + !Objects.equals(localFile.checksum, remoteFile.checksum) && + after(localFile.updated, remoteFile.updated) + ) + )) { + uploadFile(localFile, serverIP, serverKey); +// await(); + System.out.println("Uploaded"); + upCount++; + }else { + System.out.println("Did not upload"); + } + + setUpdateIndicator("Uploading " + (Math.floor((double) (i * 1000) / localFiles.size()) / 10) + "%"); + } + + setUpdateIndicator("Downloading 0%"); + + for(int i = 0; i < remoteFiles.size(); i++){ + TransferFile remoteFile = remoteFiles.get(i); + + System.out.print("RemoteFile: " + remoteFile.filename + ", " + remoteFile.checksum + ", " + remoteFile.updated + ": "); + TransferFile localFile = findInFileArray(localFiles, remoteFile.filename); + + if(localFile == null || + ( + !Objects.equals(localFile.checksum, remoteFile.checksum) && + after(remoteFile.updated, localFile.updated) && + !localFile.updated.equals(remoteFile.updated) + ) + ) { + downloadFile(remoteFile, serverIP); +// await(); + System.out.println("Downloaded"); + downCount++; + } else { + System.out.println("Did not download"); + } + + + setUpdateIndicator("Downloading " + (Math.floor((double) (i * 1000) / remoteFiles.size()) / 10) + "%"); + } + + + + + setUpdateIndicator("Finished, " + upCount + " Up, " + downCount + " Down"); + + + onResult.onResult(false, upCount, downCount); + isRunning = false; + } + + + private TransferFile findInFileArray(List files, String filename){ + for(TransferFile file : files) { + if(file.filename.equals(filename)) + return file; + } + return null; + } + + private Date getLocalFileUtcTimestamp(File file) { + return new Date(file.lastModified()); + } + + public static String getSHA256Hash(String filePath) throws IOException, NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + FileInputStream fis = new FileInputStream(filePath); + byte[] byteArray = new byte[1024]; + int bytesCount = 0; + + while ((bytesCount = fis.read(byteArray)) != -1) { + digest.update(byteArray, 0, bytesCount); + } + fis.close(); + + byte[] bytes = digest.digest(); + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + private void getLocalFileMetadata() { + File localDir = new File(baseDir); + File[] localFileNames = localDir.listFiles(); + + assert localFileNames != null; + for (int i = 0; i < localFileNames.length; i++) { + File file = localFileNames[i]; + + if(file.isDirectory()) continue; + // Remove timestamts file + if(file.getName().equals(timestampsFilename)) continue; + + TransferFile tf = new TransferFile(); + tf.filename = file.getName(); + tf.updated = getLocalFileUtcTimestamp(file); + try { + tf.checksum = getSHA256Hash(file.getPath()); + } catch (Exception e) { + + } + localFiles.add(tf); + } + } + + // Send request to server and retrieve metadata + private void getRemoteFileMetadata(String serverURL, String serverKey) { + final RequestTask rq = new RequestTask(); + runningRequest.set(false); + rq.onResult(metadata -> { + try { + JSONObject j = new JSONObject(metadata); + for (Iterator it = j.keys(); it.hasNext(); ) { + String key = it.next(); + + JSONObject obj = j.getJSONObject(key); + + TransferFile tf = new TransferFile(); + tf.filename = key; + tf.updated = new Date(Long.parseLong(obj.getString("modified"))); + tf.checksum = obj.getString("sha256"); + + remoteFiles.add(tf); + } + }catch(JSONException | NullPointerException e ) { + AlertManager.error(e); + isRunning = false; + } + runningRequest.set(true); + return null; + }); + rq.execute((serverURL + "/api/metadata"), "api_key: " + serverKey); + await(); + } + + +// private boolean setTimestamps(Map timestamps){ +// try { +// ByteBuilder bb = new ByteBuilder(); +// String[] filenames = timestamps.keySet().toArray(new String[0]); +// +// for(int i = 0; i < filenames.length; i++){ +// bb.addString(filenames[i]); +// bb.addLong(timestamps.get(filenames[i]).getTime()); +// } +// +// FileEditor.writeFile(timestampsFilename, bb.build()); +// +// uploadFile(new File(baseDir + timestampsFilename)); +// return true; +// } catch (ByteBuilder.buildingException | IOException e) { +// AlertManager.error("Failed Syncing!", e); +// return false; +// } +// } +// +// private Map getTimestamps() { +// try { +// downloadFile(timestampsFilename, new File(baseDir + timestampsFilename)); +// +// byte[] data = FileEditor.readFile(timestampsFilename); +// +// if(data == null || data.length == 0) +// return new HashMap<>(); +// +// BuiltByteParser bbp = new BuiltByteParser(data); +// List pa = bbp.parse(); +// +// Map output = new HashMap<>(); +// for(int i = 0; i < pa.size(); i+=2){ +//// System.out.println((long) pa.get(i).get()); +// output.put( +// (String) pa.get(i).get(), +// new Date((long) pa.get(i+1).get()) +// ); +// } +// return output; +// +// }catch (IOException | BuiltByteParser.byteParsingExeption e){ +// AlertManager.error("Failed Syncing!", e); +// return new HashMap<>(); +// } +// } + void uploadFile(TransferFile tf, String serverURL, String apiKey) { + runningRequest.set(false); + HttpPutFile uploadTask = new HttpPutFile(serverURL + "/api/" + tf.filename, new File(baseDir + tf.filename), new HttpPutFile.UploadCallback() { + @Override + public void onResult(String error) { + if(error != null) + AlertManager.error(error); + runningRequest.set(true); + } + }, new String[]{ + "api_key: " + apiKey, + ("modified: " + tf.updated.getTime()) + }); // Pass auth token if needed + + uploadTask.execute(); + await(); + } + + + private void setLocalFileTimestamp(File file, Date date) { + file.setLastModified(date.getTime()); + } + void downloadFile(TransferFile tf, String serverURL) { + runningRequest.set(false); + File f = new File(baseDir + tf.filename); + HttpGetFile uploadTask = new HttpGetFile(serverURL + "/api/" + tf.filename, f, new HttpGetFile.DownloadCallback() { + @Override + public void onResult(String error) { + if(error != null) + AlertManager.error(error); + else + setLocalFileTimestamp(f, tf.updated); + runningRequest.set(true); + + } + }); // Pass auth token if needed + + uploadTask.execute(); + await(); + } +} diff --git a/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/TransferFragment.java b/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/TransferFragment.java index d44413c..10a062b 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/TransferFragment.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/ui/transfer/TransferFragment.java @@ -65,13 +65,13 @@ public class TransferFragment extends Fragment { binding.SyncButton.setOnClickListener(v -> { binding.SyncButton.setEnabled(false); - FTPSync.sync(); + HttpSync.sync(); }); - if(FTPSync.getIsRunning()) + if(HttpSync.getIsRunning()) binding.SyncButton.setEnabled(false); - FTPSync.setOnResult((error, upcount, downcount) -> { + HttpSync.setOnResult((error, upcount, downcount) -> { if (getActivity() != null) getActivity().runOnUiThread(() -> { binding.SyncButton.setEnabled(true); @@ -79,8 +79,8 @@ public class TransferFragment extends Fragment { }); }); - binding.syncIndicator.setText(FTPSync.text); - FTPSync.setOnUpdateIndicator(text -> {if(getActivity() != null) getActivity().runOnUiThread(() -> binding.syncIndicator.setText(text));}); + binding.syncIndicator.setText(HttpSync.text); + HttpSync.setOnUpdateIndicator(text -> {if(getActivity() != null) getActivity().runOnUiThread(() -> binding.syncIndicator.setText(text));}); if(evcode.equals("unset")){ binding.noEventError.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/com/ridgebotics/ridgescout/utility/HttpGetFile.java b/app/src/main/java/com/ridgebotics/ridgescout/utility/HttpGetFile.java new file mode 100644 index 0000000..bbc8b66 --- /dev/null +++ b/app/src/main/java/com/ridgebotics/ridgescout/utility/HttpGetFile.java @@ -0,0 +1,168 @@ +package com.ridgebotics.ridgescout.utility; + +import android.os.AsyncTask; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; + +public class HttpGetFile extends AsyncTask { + + public interface DownloadCallback { + void onResult(String error); + } + + private String downloadUrl; + private File destinationFile; + private DownloadCallback callback; + private String errorMessage; + public HttpGetFile(String downloadUrl, File destinationFile, DownloadCallback callback) { + this.downloadUrl = downloadUrl; + this.destinationFile = destinationFile; + this.callback = callback; + } + + @Override + protected File doInBackground(Void... voids) { + HttpURLConnection connection = null; + InputStream inputStream = null; + FileOutputStream outputStream = null; + + try { + URL url = new URL(downloadUrl); + connection = (HttpURLConnection) url.openConnection(); + + // Configure connection for GET request + connection.setRequestMethod("GET"); + connection.setDoInput(true); + connection.setConnectTimeout(30000); // 30 seconds + connection.setReadTimeout(60000); // 60 seconds + + connection.connect(); + + // Check response code + int responseCode = connection.getResponseCode(); + if (responseCode < 200 || responseCode >= 300) { + String errorResponse = readErrorResponse(connection); + errorMessage = "Download failed. Response code: " + responseCode + + (errorResponse != null ? ". Error: " + errorResponse : ""); + return null; + } + + // Get file size for progress tracking + long fileSize = connection.getContentLengthLong(); + if (fileSize == -1) { + fileSize = connection.getContentLength(); // fallback for older API + } + + inputStream = connection.getInputStream(); + + // Create destination file and directories if needed + if (destinationFile.getParentFile() != null && !destinationFile.getParentFile().exists()) { + if (!destinationFile.getParentFile().mkdirs()) { + errorMessage = "Failed to create destination directory: " + destinationFile.getParentFile().getAbsolutePath(); + return null; + } + } + + outputStream = new FileOutputStream(destinationFile); + + byte[] buffer = new byte[8192]; + long downloadedBytes = 0; + int bytesRead; + + while ((bytesRead = inputStream.read(buffer)) != -1) { + if (isCancelled()) { + deletePartialFile(); + return null; + } + + outputStream.write(buffer, 0, bytesRead); + downloadedBytes += bytesRead; + + // Update progress if file size is known + if (fileSize > 0) { + int progress = (int) ((downloadedBytes * 100) / fileSize); + publishProgress(progress); + } + } + + outputStream.flush(); +// Log.d(TAG, "Download successful. File saved to: " + destinationFile.getAbsolutePath()); + return destinationFile; + + } catch (Exception e) { + AlertManager.error(e); + errorMessage = "Download error: " + e.getMessage(); +// Log.e(TAG, errorMessage, e); + deletePartialFile(); + return null; + } finally { + closeResources(inputStream, outputStream, connection); + } + } + + @Override + protected void onPostExecute(File result) { + if (callback != null) { + callback.onResult(errorMessage); + } + } + + @Override + protected void onCancelled() { + deletePartialFile(); + if (callback != null) { + callback.onResult("Download cancelled"); + } + } + + private String readErrorResponse(HttpURLConnection connection) { + try { + InputStream errorStream = connection.getErrorStream(); + if (errorStream != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream)); + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + reader.close(); + return response.toString(); + } + } catch (IOException e) { + AlertManager.error(e); +// Log.e(TAG, "Error reading error response", e); + } + return null; + } + + private void deletePartialFile() { + if (destinationFile != null && destinationFile.exists()) { + if (destinationFile.delete()) { +// Log.d(TAG, "Partial download file deleted"); + } else { +// Log.w(TAG, "Failed to delete partial download file"); + } + } + } + + private void closeResources(InputStream inputStream, OutputStream outputStream, HttpURLConnection connection) { + try { + if (inputStream != null) inputStream.close(); + } catch (IOException e) { + AlertManager.error(e); +// Log.e(TAG, "Error closing input stream", e); + } + + try { + if (outputStream != null) outputStream.close(); + } catch (IOException e) { + AlertManager.error(e); +// Log.e(TAG, "Error closing output stream", e); + } + + if (connection != null) { + connection.disconnect(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ridgebotics/ridgescout/utility/HttpPutFile.java b/app/src/main/java/com/ridgebotics/ridgescout/utility/HttpPutFile.java new file mode 100644 index 0000000..73b3583 --- /dev/null +++ b/app/src/main/java/com/ridgebotics/ridgescout/utility/HttpPutFile.java @@ -0,0 +1,161 @@ +package com.ridgebotics.ridgescout.utility; + +import android.os.AsyncTask; +//import android.util.Log; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; + +public class HttpPutFile extends AsyncTask { + +// private static final String TAG = "FileUploadTask"; + + public interface UploadCallback { + void onResult(String error); + } + + private String uploadUrl; + private File fileToUpload; + private UploadCallback callback; + private String errorMessage; + private String[] headers; + + public HttpPutFile(String uploadUrl, File fileToUpload, UploadCallback callback, String[] headers) { + this.uploadUrl = uploadUrl; + this.fileToUpload = fileToUpload; + this.callback = callback; + this.headers = headers; + } + + @Override + protected Boolean doInBackground(Void... voids) { + HttpURLConnection connection = null; + InputStream fileInputStream = null; + OutputStream outputStream = null; + + try { + if (!fileToUpload.exists()) { + errorMessage = "File does not exist: " + fileToUpload.getAbsolutePath(); + return false; + } + + URL url = new URL(uploadUrl); + connection = (HttpURLConnection) url.openConnection(); + + // Configure connection for PUT request + connection.setRequestMethod("PUT"); + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setUseCaches(false); + connection.setRequestProperty("Content-Type", "application/octet-stream"); + connection.setRequestProperty("Content-Length", String.valueOf(fileToUpload.length())); + connection.setConnectTimeout(30000); // 30 seconds + connection.setReadTimeout(60000); // 60 seconds + + for(int i = 0; i < headers.length; i++){ + String[] split = headers[i].split(": "); + connection.setRequestProperty(split[0], split[1]); + } + connection.connect(); + + outputStream = connection.getOutputStream(); + fileInputStream = new FileInputStream(fileToUpload); + + byte[] buffer = new byte[8192]; + long totalBytes = fileToUpload.length(); + long uploadedBytes = 0; + int bytesRead; + + while ((bytesRead = fileInputStream.read(buffer)) != -1) { + if (isCancelled()) { + return false; + } + + outputStream.write(buffer, 0, bytesRead); + uploadedBytes += bytesRead; + + // Update progress + int progress = (int) ((uploadedBytes * 100) / totalBytes); + publishProgress(progress); + } + + outputStream.flush(); + + // Check response code + int responseCode = connection.getResponseCode(); + if (responseCode >= 200 && responseCode < 300) { +// Log.d(TAG, "Upload successful. Response code: " + responseCode); + return true; + } else { + // Read error response if available + String errorResponse = readErrorResponse(connection); + errorMessage = "Upload failed. Response code: " + responseCode + + (errorResponse != null ? ". Error: " + errorResponse : ""); + return false; + } + + } catch (Exception e) { + AlertManager.error(e); + errorMessage = "Upload error: " + e.getMessage(); +// Log.e(TAG, errorMessage, e); + return false; + } finally { + closeResources(fileInputStream, outputStream, connection); + } + } + + + @Override + protected void onPostExecute(Boolean success) { + if (callback != null) { + callback.onResult(errorMessage); + } + } + + @Override + protected void onCancelled() { + if (callback != null) { + callback.onResult("Upload cancelled"); + } + } + + private String readErrorResponse(HttpURLConnection connection) { + try { + InputStream errorStream = connection.getErrorStream(); + if (errorStream != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream)); + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + reader.close(); + return response.toString(); + } + } catch (IOException e) { + AlertManager.error(e); +// Log.e(TAG, "Error reading error response", e); + } + return null; + } + + private void closeResources(InputStream inputStream, OutputStream outputStream, HttpURLConnection connection) { + try { + if (inputStream != null) inputStream.close(); + } catch (IOException e) { + AlertManager.error(e); +// Log.e(TAG, "Error closing input stream", e); + } + + try { + if (outputStream != null) outputStream.close(); + } catch (IOException e) { + AlertManager.error(e); +// Log.e(TAG, "Error closing output stream", e); + } + + if (connection != null) { + connection.disconnect(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ridgebotics/ridgescout/utility/SettingsManager.java b/app/src/main/java/com/ridgebotics/ridgescout/utility/SettingsManager.java index dd13829..897d12d 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/utility/SettingsManager.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/utility/SettingsManager.java @@ -28,6 +28,7 @@ public class SettingsManager { public static final String BtUUIDKey = "bt_uuid"; public static final String FTPEnabled = "ftp_enabled"; public static final String FTPServer = "ftp_server"; + public static final String FTPKey = "ftp_key"; public static final String FTPSendMetaFiles = "ftp_send_meta_files"; public static final String EnableQuickAllianceChangeKey = "enable_quick_alliance_change"; @@ -53,7 +54,8 @@ public class SettingsManager { hm.put(TeamsDataModeKey, 0); hm.put(BtUUIDKey, UUID.randomUUID().toString()); hm.put(FTPEnabled, false); - hm.put(FTPServer, "0.0.0.0"); + hm.put(FTPServer, "http://127.0.0.1:8080"); + hm.put(FTPKey, "5uper_5ecure_k3y"); hm.put(FTPSendMetaFiles, false); hm.put(EnableQuickAllianceChangeKey, false); hm.put(CustomEventsKey, false); @@ -131,6 +133,9 @@ public class SettingsManager { public static String getFTPServer(){return prefs.getString( FTPServer, (String) defaults.get(FTPServer));} public static void setFTPServer(String str){ getEditor().putString( FTPServer,str).apply();} + public static String getFTPKey(){return prefs.getString( FTPKey, (String) defaults.get(FTPKey));} + public static void setFTPKey(String str){ getEditor().putString( FTPKey,str).apply();} + public static boolean getFTPSendMetaFiles(){return prefs.getBoolean(FTPSendMetaFiles, (boolean) defaults.get(FTPSendMetaFiles));} public static void setFTPSendMetaFiles(boolean bool){getEditor().putBoolean(FTPSendMetaFiles,bool).apply();} diff --git a/server/main.py b/server/main.py index 457abd5..012c88f 100644 --- a/server/main.py +++ b/server/main.py @@ -76,7 +76,7 @@ def upload(filename): try: sentkey = request.headers[API_KEY_HEADER] if sentkey != api_key: - return HTTPResponse(status=403, body=f"Invalid Key {sentkey}, {api_key}") + return HTTPResponse(status=403, body=f"Invalid Key") except: return HTTPResponse(status=403, body="You must specify an 'api_key' header") @@ -124,4 +124,4 @@ def download(filename): if __name__ == '__main__': mkdir(DATA_ROOT) aquire_key() - app.run(host='localhost', port=8080) + app.run(host='0.0.0.0', port=8080) diff --git a/server/utils.py b/server/utils.py index 48114b7..a1e6c5e 100644 --- a/server/utils.py +++ b/server/utils.py @@ -1,7 +1,7 @@ import os ROOT = os.path.dirname(__file__) -DATA_ROOT = os.path.join(os.path.dirname(__file__), 'data') +DATA_ROOT = os.path.join(os.path.dirname(__file__), 'server_data') METADATA_PATH4 = os.path.join(ROOT, 'metadata.json') API_KEY_PATH = os.path.join(ROOT, 'api_key.txt') From 65baecac355e2366117994d4f10c81d9d01ca787 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Mon, 26 May 2025 10:13:28 -0600 Subject: [PATCH 4/4] Fix crash --- .../ridgescout/ui/scouting/ScoutingFragment.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/ridgebotics/ridgescout/ui/scouting/ScoutingFragment.java b/app/src/main/java/com/ridgebotics/ridgescout/ui/scouting/ScoutingFragment.java index fac89a6..47532e0 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/ui/scouting/ScoutingFragment.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/ui/scouting/ScoutingFragment.java @@ -22,6 +22,7 @@ import androidx.fragment.app.Fragment; import com.ridgebotics.ridgescout.R; import com.ridgebotics.ridgescout.types.frcEvent; +import com.ridgebotics.ridgescout.utility.AlertManager; import com.ridgebotics.ridgescout.utility.FileEditor; import com.ridgebotics.ridgescout.utility.SettingsManager; import com.ridgebotics.ridgescout.databinding.FragmentScoutingBinding; @@ -126,7 +127,12 @@ public class ScoutingFragment extends Fragment { binding.textName.setText("Welcome, " + SettingsManager.getUsername() + "!"); int matchNum = SettingsManager.getMatchNum(); - int nextMatch = event.getNextTeamMatch(SettingsManager.getTeamNum(), matchNum).matchIndex; + int nextMatch = -1; + try { + nextMatch = event.getNextTeamMatch(SettingsManager.getTeamNum(), matchNum).matchIndex; + } catch (Exception e){ + AlertManager.error(e); + } binding.textNextMatch.setText("Our next match: Match " + nextMatch); binding.textMatchAlliance.setText("Match: " + (matchNum+1) + ", " + SettingsManager.getAllyPos());