Skip to content

Commit b891bea

Browse files
committed
ANDROID: file manager webui (wip)
1 parent 2b002bb commit b891bea

File tree

3 files changed

+178
-74
lines changed

3 files changed

+178
-74
lines changed

src/platform/android/app/src/main/java/net/sourceforge/smallbasic/WebServer.java

Lines changed: 101 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public void run() {
5858
protected abstract Response getResponse(String path, boolean asset) throws IOException;
5959
protected abstract void log(String message, Exception exception);
6060
protected abstract void log(String message);
61+
protected abstract boolean saveFile(String fileName, String content);
6162

6263
/**
6364
* Returns the HTTP headers from the given stream
@@ -204,35 +205,18 @@ private Response handleFileList() throws IOException {
204205
}
205206

206207
/**
207-
* Handler for GET requests
208+
* Handler for file uploads
208209
*/
209-
private void handleGet(Socket socket, String token, List<String> headers, String url,
210-
Map<String, Collection<String>> parameters) throws IOException {
211-
if (url.startsWith("/api/download?")) {
212-
if (hasTokenCookie(headers, token)) {
213-
handleDownload(parameters).send(socket, null);
214-
}
215-
} else {
216-
handleWebResponse(url).send(socket, null);
217-
}
218-
}
219-
220-
/**
221-
* Handler for POST requests
222-
*/
223-
private void handlePost(Socket socket, String token, String url,
224-
Map<String, String> parameters) throws IOException {
225-
String userToken = parameters.get(TOKEN);
226-
log("userToken=" + userToken);
227-
if (token.equals(userToken)) {
228-
if (url.startsWith("/api/files")) {
229-
handleFileList().send(socket, token);
230-
}
231-
log("Sent POST response");
232-
} else {
233-
log("Invalid token");
234-
handleError("invalid token").send(socket, null);
235-
}
210+
private Response handleUpload(Map<String, String> parameters) throws UnsupportedEncodingException {
211+
String fileName = parameters.get("fileName");
212+
String content = parameters.get("data");
213+
boolean result = saveFile(fileName, content);
214+
JsonBuilder json = new JsonBuilder();
215+
json.append('{');
216+
json.append("success", Boolean.toString(result), false);
217+
json.append('}');
218+
byte[] output = json.getBytes();
219+
return new Response(new ByteArrayInputStream(output), output.length);
236220
}
237221

238222
/**
@@ -250,20 +234,6 @@ private Response handleWebResponse(String asset) throws IOException {
250234
return getResponse(path, true);
251235
}
252236

253-
/**
254-
* Inspects the headers to ensure the token cookie is present
255-
*/
256-
private boolean hasTokenCookie(List<String> headers, String token) {
257-
boolean result = false;
258-
for (String header : headers) {
259-
if (header.startsWith("Cookie: ") && header.contains(TOKEN) && header.contains(token)) {
260-
result = true;
261-
break;
262-
}
263-
}
264-
return result;
265-
}
266-
267237
/**
268238
* WebServer main loop to be run in a separate thread
269239
*/
@@ -284,21 +254,8 @@ private void runServer(final int socketNum, final String token) throws IOExcepti
284254
socket = serverSocket.accept();
285255
log("Accepted connection from " + socket.getRemoteSocketAddress().toString());
286256
inputStream = new DataInputStream(socket.getInputStream());
287-
List<String> headers = getHeaders(inputStream);
288-
if (!headers.isEmpty()) {
289-
String line = headers.get(0);
290-
log(line);
291-
String[] fields = line.split("\\s");
292-
if ("GET".equals(fields[0]) && fields.length > 1) {
293-
Map<String, Collection<String>> parameters = getParameters(fields[1]);
294-
handleGet(socket, token, headers, fields[1], parameters);
295-
} else if ("POST".equals(fields[0]) && fields.length > 1) {
296-
Map<String, String> parameters = getPostData(inputStream, line);
297-
handlePost(socket, token, fields[1], parameters);
298-
} else {
299-
log("Invalid request");
300-
}
301-
}
257+
Request request = new Request(socket, getHeaders(inputStream));
258+
request.invoke(inputStream, token);
302259
} catch (Throwable e) {
303260
log("Server failed");
304261
break;
@@ -371,6 +328,93 @@ byte[] getBytes() throws UnsupportedEncodingException {
371328
}
372329
}
373330

331+
/**
332+
* Server Request
333+
*/
334+
public class Request {
335+
Socket socket;
336+
String method;
337+
String url;
338+
String token;
339+
List<String> headers;
340+
341+
public Request(Socket socket, List<String> headers) {
342+
String[] fields = headers.get(0).split("\\s");
343+
this.socket = socket;
344+
this.headers = headers;
345+
this.token = getToken(headers);
346+
if (fields.length > 1) {
347+
this.method = fields[0];
348+
this.url = fields[1];
349+
} else {
350+
this.method = null;
351+
this.url = null;
352+
}
353+
}
354+
355+
public void invoke(InputStream inputStream, String tokenKey) throws IOException {
356+
if (!headers.isEmpty()) {
357+
log(url);
358+
if ("GET".equals(method)) {
359+
handleGet(getParameters(url), tokenKey);
360+
} else if ("POST".equals(method)) {
361+
handlePost(getPostData(inputStream, url), tokenKey);
362+
} else {
363+
log("Invalid request");
364+
}
365+
}
366+
}
367+
368+
private String getToken(List<String> headers) {
369+
String result = null;
370+
for (String header : headers) {
371+
if (header.startsWith("cookie:")) {
372+
String[] fields = header.split(":");
373+
if (fields.length == 2) {
374+
fields = fields[1].split("=");
375+
if (fields.length == 2 && TOKEN.equals(fields[0].trim())) {
376+
result = fields[1];
377+
break;
378+
}
379+
}
380+
}
381+
}
382+
return result;
383+
}
384+
385+
/**
386+
* Handler for GET requests
387+
*/
388+
private void handleGet(Map<String, Collection<String>> parameters, String tokenKey) throws IOException {
389+
if (url.startsWith("/api/download?")) {
390+
if (tokenKey.equals(token)) {
391+
handleDownload(parameters).send(socket, null);
392+
}
393+
} else {
394+
handleWebResponse(url).send(socket, null);
395+
}
396+
}
397+
398+
/**
399+
* Handler for POST requests
400+
*/
401+
private void handlePost(Map<String, String> parameters, String tokenKey) throws IOException {
402+
String userToken = parameters.getOrDefault(TOKEN, token);
403+
log("userToken=" + userToken);
404+
if (tokenKey.equals(userToken)) {
405+
if (url.startsWith("/api/files")) {
406+
handleFileList().send(socket, token);
407+
} else if (url.startsWith("/api/upload")) {
408+
handleUpload(parameters).send(socket, token);
409+
}
410+
log("Sent POST response");
411+
} else {
412+
log("Invalid token");
413+
handleError("invalid token").send(socket, null);
414+
}
415+
}
416+
}
417+
374418
/**
375419
* Server response data
376420
*/

src/platform/android/webui/server/src/main/java/net/sourceforge/smallbasic/Server.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ protected void log(String message) {
4949
System.err.println(message);
5050
}
5151

52+
@Override
53+
protected boolean saveFile(String fileName, String content) {
54+
return true;
55+
}
56+
5257
@Override
5358
protected void log(String message, Exception exception) {
5459
System.err.println(message);

src/platform/android/webui/src/App.js

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
useEffect,
32
useState
43
} from 'react';
54

@@ -22,7 +21,8 @@ import {
2221
} from '@mui/x-data-grid';
2322

2423
import {
25-
Download as DownloadIcon
24+
Download as DownloadIcon,
25+
Upload as UploadIcon
2626
} from '@mui/icons-material';
2727

2828
import {
@@ -67,6 +67,40 @@ function getFetchHeader(body) {
6767
};
6868
}
6969

70+
function getFiles(token, success, fail) {
71+
fetch('/api/files', getFetchHeader("token=" + token))
72+
.then(response => response.json())
73+
.then(success)
74+
.catch(fail);
75+
}
76+
77+
function upload(name, data, success, fail) {
78+
let body = "fileName=" + encodeURIComponent(name) + "&data=" + encodeURIComponent(data);
79+
fetch('/api/upload', getFetchHeader(body))
80+
.then(response => response.json())
81+
.then(success)
82+
.catch(fail);
83+
}
84+
85+
function copyFiles(event, token, success, fail) {
86+
const fileReader = new FileReader();
87+
const input = event.target;
88+
const files = input.files;
89+
let index = 0;
90+
fileReader.onload = () => {
91+
upload(files[index].name, fileReader.result, () => {
92+
if (++index < files.length) {
93+
fileReader.readAsText(files[index]);
94+
} else {
95+
getFiles(token, success, fail);
96+
// reset input control
97+
input.value = input.defaultValue;
98+
}
99+
}, fail);
100+
};
101+
fileReader.readAsText(files[index]);
102+
}
103+
70104
function GridToolbarDownload(props) {
71105
let color = useTheme().palette.primary.main;
72106
let args = "";
@@ -89,11 +123,31 @@ function GridToolbarDownload(props) {
89123
);
90124
}
91125

126+
function GridToolbarUpload(props) {
127+
const handleUpload = (event) => {
128+
copyFiles(event, props.token, (newRows) => {
129+
props.setRows(newRows);
130+
}, (error) => {
131+
// show toast message
132+
console.log(error);
133+
});
134+
};
135+
136+
return (
137+
<Button color="primary" size="small" component="label" sx={{marginLeft: '-4px'}}>
138+
<input accept=".bas" hidden multiple type="file" onChange={handleUpload}/>
139+
<UploadIcon />
140+
UPLOAD
141+
</Button>
142+
);
143+
}
144+
92145
function AppToolbar(props) {
93146
return (
94147
<GridToolbarContainer>
95148
<GridToolbarFilterButton />
96149
<GridToolbarDensitySelector />
150+
<GridToolbarUpload {...props}/>
97151
<GridToolbarDownload {...props}/>
98152
</GridToolbarContainer>
99153
);
@@ -102,12 +156,19 @@ function AppToolbar(props) {
102156
function FileList(props) {
103157
const [selectionModel, setSelectionModel] = useState([]);
104158

159+
const toolbarProps = {
160+
selections: selectionModel,
161+
setRows: props.setRows,
162+
rows: props.rows,
163+
token: props.token,
164+
};
165+
105166
return (
106167
<DataGrid rows={props.rows}
107168
columns={columns}
108169
pageSize={5}
109170
components={{Toolbar: AppToolbar}}
110-
componentsProps={{toolbar: {selections: selectionModel, rows: props.rows}}}
171+
componentsProps={{toolbar: toolbarProps}}
111172
onSelectionModelChange={(model) => setSelectionModel(model)}
112173
selectionModel={selectionModel}
113174
rowsPerPageOptions={[5]}
@@ -121,19 +182,12 @@ function TokenInput(props) {
121182
const [error, setError] = useState(false);
122183

123184
const onClick = () => {
124-
const validate = async () => {
125-
let response = await fetch('/api/files', getFetchHeader("token=" + token));
126-
let data = await response.json();
127-
if (data.error) {
128-
setError(true);
129-
} else {
130-
props.setRows(data);
131-
props.setToken(token);
132-
}
133-
};
134-
if (token) {
135-
validate().catch(console.error);
136-
}
185+
getFiles(token, (data) => {
186+
props.setRows(data);
187+
props.setToken(token);
188+
}, () => {
189+
setError(true);
190+
});
137191
};
138192

139193
const onChange = (event) => {
@@ -162,6 +216,7 @@ function TokenInput(props) {
162216
onChange={onChange}
163217
onKeyPress={onKeyPress}
164218
helperText={helperText}
219+
autoFocus
165220
label="Enter your access token"/>
166221
</Box>
167222
<Box>
@@ -178,7 +233,7 @@ export default function App() {
178233

179234
let content;
180235
if (token) {
181-
content = <FileList rows={rows}/>;
236+
content = <FileList setRows={setRows} rows={rows} token={token} />;
182237
} else {
183238
content = <TokenInput setRows={setRows} setToken={setToken} token={token}/>;
184239
}

0 commit comments

Comments
 (0)