Add java side of http syncing

This commit is contained in:
Michael Mikovsky
2025-05-25 18:48:02 -06:00
parent 5d727cf359
commit e278bc10a1
10 changed files with 726 additions and 13 deletions
+6
View File
@@ -1,3 +1,9 @@
# Python server
__pycache__/
metadata.json
api_key.txt
server_data/
# Gradle files
.gradle/
build/
@@ -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));
@@ -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";
@@ -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<TransferFile> localFiles = new ArrayList<>();
private List<TransferFile> 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<TransferFile> 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<String> 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<String, Date> 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<String, Date> 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<BuiltByteParser.parsedObject> pa = bbp.parse();
//
// Map<String, Date> 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();
}
}
@@ -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);
@@ -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<Void, Integer, File> {
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();
}
}
}
@@ -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<Void, Integer, Boolean> {
// 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();
}
}
}
@@ -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();}
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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')