Add FTP server

This commit is contained in:
Astatin3
2024-10-06 16:06:43 -06:00
parent 03a1507ce2
commit fb0718c4ec
9 changed files with 176 additions and 151 deletions
Binary file not shown.
+4 -2
View File
@@ -1,4 +1,4 @@
![Ridgescout](https://github.com/Team4388/ScoutingApp2025/blob/main/metadata/en-US/images/FeatureGraphic.png?raw=true)
![Ridgescout](https://github.com/Team4388/ScoutingApp2025/blob/main/metadata/en-US/images/featureGraphic.png?raw=true)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
@@ -7,6 +7,8 @@
[**Read the wiki**](https://github.com/Team4388/ScoutingApp2025/wiki)
[**Test Data**](https://github.com/Team4388/ScoutingApp2025/blob/main/2024week0-1728149849985.scoutbundle)
#### Here is an overview of the main features currently included in the app:
- This project is written for Android! No need for some kind of janky laptop charging setup.
- Similar to ScoutingPASS, there are many diffrent types of fields that can be used to collect data.
@@ -16,11 +18,11 @@
- Data transfer including 2D codes, Bluetooth, and File Bundle.
- Exporting using CSV.
- Deployment on F-Droid
- Data cloud sync using an FTP server
#### Things that are yet to be implemented:
- A page that lets users cross-compare scouting data between teams. (Compare)
- A page that lets scouters more easily make reports to the drive team before a match starts (Report)
- Data cloud sync using an FTP server
#### Things that may or may not be implemented:
- Practice mode
+2 -2
View File
@@ -4,7 +4,6 @@
##### Data Analysis:
- Statbotics intigration???
##### Functionality:
- Test the scouting app
### In Progress:
##### Scouting:
@@ -13,7 +12,6 @@
- Make the "Report" menu, A tool that lets users select data to display from the the teams and compare menus.
- Make the "Compare" menu, cross comparing team's stats.
##### Functionality:
- Make server software to allow for easy sync over wifi - FTP
### Done:
##### Scouting:
@@ -36,3 +34,5 @@
- Write docs
- Deploy to F-Droid
- Add more types of data fields.
- Make server software to allow for easy sync over wifi - FTP
- Test the scouting app
+2 -2
View File
@@ -28,8 +28,8 @@ android {
applicationId = "com.ridgebotics.ridgescout"
minSdk = 24
targetSdk = 34
versionCode = 4
versionName = "0.4"
versionCode = 5
versionName = "0.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -25,6 +25,7 @@ import com.ridgebotics.ridgescout.utility.settingsManager;
import com.google.android.material.navigation.NavigationBarView;
import java.util.Objects;
import java.util.TimeZone;
public class MainActivity extends AppCompatActivity {
@@ -51,6 +52,9 @@ public class MainActivity extends AppCompatActivity {
fields.save(fields.pitsFieldsFilename, fields.default_pit_fields);
}
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
AlertManager.init(this);
SentimentAnalysis.init(this);
@@ -1,166 +1,163 @@
package com.ridgebotics.ridgescout.ui.transfer;
import static com.ridgebotics.ridgescout.utility.DataManager.evcode;
//import static com.ridgebotics.ridgescout.utility.DataManager.evcode;
import static com.ridgebotics.ridgescout.utility.fileEditor.baseDir;
import android.content.Context;
import android.net.Uri;
import com.ridgebotics.ridgescout.utility.AlertManager;
import com.ridgebotics.ridgescout.utility.DataManager;
import com.ridgebotics.ridgescout.utility.fileEditor;
import com.ridgebotics.ridgescout.utility.settingsManager;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPCmd;
import org.apache.commons.net.ftp.FTPFile;
import org.checkerframework.checker.units.qual.C;
import org.apache.commons.net.ftp.FTPReply;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Date;
import java.util.TimeZone;
public class FTPSync extends Thread {
private static final String remoteBasePath = "/RidgeScout/";
public static final String remoteBasePath = "/RidgeScout/";
public static final long timeTolerance = 60000; // One min
public static long lastSyncTime = 0;
public interface onResult {
void onResult(boolean error);
void onResult(boolean error, int upCount, int downCount);
}
public onResult onResult;
public static void sync(onResult onResult){
DataManager.reload_event();
// DataManager.reload_event();
FTPSync ftpSync = new FTPSync();
ftpSync.onResult = onResult;
ftpSync.start();
lastSyncTime = new Date().getTime();
}
private class FileDate {
public String filename;
public Calendar lastModified;
FTPClient ftpClient;
// private class FileDate {
// public String filename;
// public Calendar lastModified;
// }
private int upCount = 0;
private int downCount = 0;
private void downloadFile(FTPFile remoteFile, File localFile) throws IOException {
try (FileOutputStream fos = new FileOutputStream(localFile)) {
ftpClient.retrieveFile(remoteBasePath + remoteFile.getName(), fos);
}
Date d = getUtcTimestamp(remoteFile);
System.out.println(d);
setLocalFileTimestamp(localFile, d);
}
public boolean sendFile(FTPClient ftpClient, String filename) throws Exception{
FileInputStream fin = new FileInputStream(baseDir + filename);
boolean worked = ftpClient.storeFile(remoteBasePath + filename, fin);
fin.close();
return worked;
private void uploadFile(File localFile) throws IOException {
try (FileInputStream fis = new FileInputStream(localFile)) {
ftpClient.storeFile(remoteBasePath + localFile.getName(), fis);
}
setLocalFileTimestamp(localFile, new Date());
}
private FTPFile findRemoteFile(FTPFile[] remoteFiles, String fileName) {
for (FTPFile file : remoteFiles) {
if (file.getName().equals(fileName)) {
return file;
}
}
return null;
}
private Date getUtcTimestamp(FTPFile file) {
return file.getTimestamp().getTime();
}
private Date getLocalFileUtcTimestamp(File file) {
return new Date(file.lastModified());
}
private void setLocalFileTimestamp(File file, Date date) {
file.setLastModified(date.getTime());
}
// private long longAbs(long n){
// return n>0 ? n : -n;
// }
private boolean toleranceCompareAfter(Date d1, Date d2){
long diff = d1.getTime() - d2.getTime();
System.out.println(diff);
return diff > timeTolerance;
}
public void run() {
try {
FTPClient ftpClient = new FTPClient();
// localTimeZone = TimeZone.getDefault();
ftpClient = new FTPClient();
InetAddress address = InetAddress.getByName(settingsManager.getFTPServer());
ftpClient.connect(address);
ftpClient.login("anonymous", null);
ftpClient.enterLocalPassiveMode();
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// ftpClient.setFileTransferMode(FTP.BLOCK_TRANSFER_MODE)
FTPFile[] remoteFilestmp = ftpClient.listFiles(remoteBasePath);
String[] localFilestmp = fileEditor.getEventFiles(evcode);
File localDir = new File(baseDir);
File[] localFiles = localDir.listFiles();
FTPFile[] remoteFiles = ftpClient.listFiles(remoteBasePath);
FileDate[] remoteFiles = new FileDate[remoteFilestmp.length];
for(int i = 0; i < remoteFilestmp.length; i++){
// System.out.println(remoteFilestmp[i].getName());
remoteFiles[i] = new FileDate();
remoteFiles[i].filename = remoteFilestmp[i].getName();
System.out.println(remoteFiles[i].filename);
remoteFiles[i].lastModified = remoteFilestmp[i].getTimestamp();
if (localFiles != null) {
for (File localFile : localFiles) {
if (localFile.isFile()) {
FTPFile remoteFile = findRemoteFile(remoteFiles, localFile.getName());
//
// Date t1 = getLocalFileUtcTimestamp(localFile);
// Date t2 = getUtcTimestamp(remoteFile);
////
// System.out.println("- " + t1.getTime() + (t1.after(t2) ? ">" : "<") + t2.getTime());
if (remoteFile == null || toleranceCompareAfter(getLocalFileUtcTimestamp(localFile), (getUtcTimestamp(remoteFile)))) {
uploadFile(localFile);
System.out.println("Uploaded " + localFile.getName());
upCount++;
}else{
System.out.println("Did not upload " + localFile.getName());
}
FileDate[] localFiles = new FileDate[localFilestmp.length];
for(int i = 0; i < localFilestmp.length; i++){
// sendFile(localFilestmp[i]);
String filename = "matches.fields";
File f = new File(baseDir + filename);
// File f = new File(baseDir + localFilestmp[i]);
// System.out.println(f.exists());
// FileInputStream fin = new FileInputStream(f);
// System.out.println(f.getName() + ", " + f.exists() + ", " + sendFile(ftpClient, f));
// fin.close();
// System.out.println(ftpClient.getStatus());
localFiles[i] = new FileDate();
localFiles[i].filename = localFilestmp[i];
localFiles[i].lastModified = fileEditor.getLastModified(localFilestmp[i]);
}
FileAction[] localActions = compareDates(localFiles, fd2map(remoteFiles), false);
System.out.println(localActions.length);
for(int i = 0 ; i < localActions.length; i++){
System.out.println(localActions[i].filename + ", " + localActions[i].action);
switch (localActions[i].action){
case SEND:
sendFile(ftpClient, localActions[i].filename);
fileEditor.setLastModified(localActions[i].filename, localActions[i].otherTimestamp);
break;
case RECIEVE:
break;
}
}
for (FTPFile remoteFile : remoteFiles) {
if (!remoteFile.isDirectory()) {
File localFile = new File(baseDir, remoteFile.getName());
// Date t1 = getLocalFileUtcTimestamp(localFile);
// Date t2 = getUtcTimestamp(remoteFile);
////
// System.out.println("- " + t1 + (t1.after(t2) ? ">" : "<") + t2);
if (!localFile.exists() || toleranceCompareAfter(getUtcTimestamp(remoteFile), (getLocalFileUtcTimestamp(localFile)))) {
downloadFile(remoteFile, localFile);
System.out.println("Downloaded " + localFile.getName());
downCount++;
}else{
System.out.println("Did not download " + remoteFile.getName());
}
}
}
} catch (Exception e) {
AlertManager.error(e);
onResult.onResult(true);
AlertManager.toast("Error Syncing!");
onResult.onResult(true, upCount, downCount);
} finally {
onResult.onResult(false, upCount, downCount);
}
// } finally {
// AlertManager.toast("Synced!");
// onResult.onResult(false);
// }
}
private Map<String, Calendar> fd2map(FileDate[] fdarr){
Map<String, Calendar> map = new HashMap<>();
for(int i = 0; i < fdarr.length; i++)
map.put(fdarr[i].filename, fdarr[i].lastModified);
return map;
}
private enum SyncAction {
SEND,
RECIEVE,
NONE
}
private class FileAction {
String filename;
SyncAction action;
Calendar otherTimestamp;
public FileAction(String filename, SyncAction action, Calendar otherTimestamp){
this.filename = filename;
this.action = action;
this.otherTimestamp = otherTimestamp;
}
}
private FileAction[] compareDates(FileDate[] files, Map<String, Calendar> refrence, boolean reverse){
FileAction[] actions = new FileAction[files.length];
for(int i = 0; i < files.length; i++){
Calendar ref = refrence.get(files[i].filename);
System.out.println(ref);
if(ref == null || files[i].lastModified.after(ref)) {
actions[i] = new FileAction(files[i].filename, !reverse ? SyncAction.SEND : SyncAction.RECIEVE, ref);
}else if(files[i].lastModified.before(ref)){
actions[i] = new FileAction(files[i].filename, !reverse ? SyncAction.RECIEVE : SyncAction.SEND, ref);
}else {
actions[i] = new FileAction(files[i].filename, SyncAction.NONE, ref);
}
}
return actions;
}
}
@@ -14,11 +14,14 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.ridgebotics.ridgescout.R;
import com.ridgebotics.ridgescout.utility.AlertManager;
import com.ridgebotics.ridgescout.utility.settingsManager;
import com.ridgebotics.ridgescout.databinding.FragmentTransferBinding;
import com.ridgebotics.ridgescout.ui.transfer.bluetooth.BluetoothSenderFragment;
import com.ridgebotics.ridgescout.ui.transfer.codes.CodeGeneratorView;
import java.util.Date;
public class TransferFragment extends Fragment {
private FragmentTransferBinding binding;
@@ -66,12 +69,31 @@ public class TransferFragment extends Fragment {
alert.create().show();
});
if(!settingsManager.getWifiMode()) {
binding.TBAButton.setEnabled(false);
binding.SyncButton.setEnabled(false);
}
if(!settingsManager.getFTPEnabled() ||
new Date().getTime()-FTPSync.lastSyncTime < FTPSync.timeTolerance) {
binding.SyncButton.setEnabled(false);
}
binding.SyncButton.setOnClickListener(v -> {
binding.SyncButton.setEnabled(false);
FTPSync.sync((error, upcount, downcount) -> getActivity().runOnUiThread(() -> {
// binding.SyncButton.setEnabled(true);
AlertManager.toast((!error ? "Synced! " : "Error Syncing. ") + upcount + " Up " + downcount + " Down");
}));
});
if(evcode.equals("unset")){
binding.noEventError.setVisibility(View.VISIBLE);
binding.uploadButton.setEnabled(false);
binding.CSVButton.setEnabled(false);
binding.downloadButton.setEnabled(true);
binding.SyncButton.setEnabled(false);
return binding.getRoot();
}
@@ -107,20 +129,6 @@ public class TransferFragment extends Fragment {
builder.show();
});
if(!settingsManager.getWifiMode()) {
binding.TBAButton.setEnabled(false);
binding.SyncButton.setEnabled(false);
}
if(!settingsManager.getFTPEnabled()) {
binding.SyncButton.setEnabled(false);
}
binding.SyncButton.setOnClickListener(v -> {
binding.SyncButton.setEnabled(false);
FTPSync.sync(error -> getActivity().runOnUiThread(() -> binding.SyncButton.setEnabled(true)));
});
return binding.getRoot();
}
@@ -2,7 +2,9 @@ package com.ridgebotics.ridgescout.ui.transfer.codes;
import static androidx.core.math.MathUtils.clamp;
import android.Manifest;
import android.app.AlertDialog;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.media.Image;
import android.os.Bundle;
@@ -25,6 +27,7 @@ import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LifecycleOwner;
@@ -150,6 +153,9 @@ public class CodeScannerView extends Fragment {
this.lifecycle = getViewLifecycleOwner();
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.CAMERA}, 1);
}
uiHandler = new Handler();
@@ -15,6 +15,8 @@ import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@@ -22,6 +24,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
@@ -30,6 +33,7 @@ public final class fileEditor {
public final static String baseDir = "/data/data/com.ridgebotics.ridgescout/";
public static final byte internalDataVersion = 0x01;
public static final int maxCompressedBlockSize = 4096;
// private TimeZone localTimeZone = TimeZone.getDefault();
@@ -190,22 +194,22 @@ public final class fileEditor {
public static Calendar getLastModified(String filepath){
File f = new File(baseDir + filepath);
if(f.exists()){
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(f.lastModified());
return calendar;
}
return null;
}
public static void setLastModified(String filepath, Calendar calendar){
File f = new File(baseDir + filepath);
if(f.exists()){
f.setLastModified(calendar.getTimeInMillis());
}
}
// public static Calendar getLastModified(String filepath){
// File f = new File(baseDir + filepath);
// if(f.exists()){
// Calendar calendar = Calendar.getInstance();
// calendar.setTimeInMillis(f.lastModified());
// return calendar;
// }
// return null;
// }
//
// public static void setLastModified(String filepath, Calendar calendar){
// File f = new File(baseDir + filepath);
// if(f.exists()){
// f.setLastModified(calendar.getTimeInMillis());
// }
// }
@@ -214,6 +218,10 @@ public final class fileEditor {
FileOutputStream output = new FileOutputStream(baseDir + filepath);
output.write(data);
output.close();
// Date d = new Date();
new File(baseDir + filepath).setLastModified(new Date().getTime());
return true;
}
catch (IOException e) {