mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
PE-41 Remote File API (#7)
* added API for read-file * implement writeRemoteFile -- multipart upload, params/data * format file, implement readOnly flag on file return from readRemoteFile * fix model.ts typescript errors * add usetemp to write-file api * add GlobalCommandRunner.setLineState() * implment PE-13, PE-60
This commit is contained in:
@@ -594,6 +594,7 @@ class LineCmd extends React.Component<
|
||||
savedHeight: savedHeight,
|
||||
opts: this.getRendererOpts(cmd),
|
||||
ptyDataSource: getTermPtyData,
|
||||
lineState: line.linestate,
|
||||
api: api,
|
||||
rawCmd: cmd.getAsWebCmd(line.lineid),
|
||||
};
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.line .load-error-text {
|
||||
color: #cc0000;
|
||||
}
|
||||
|
||||
.line.line-cmd {
|
||||
flex-direction: column;
|
||||
scroll-margin-bottom: 20px;
|
||||
|
||||
+83
-1
@@ -59,6 +59,7 @@ import type {
|
||||
WebCmd,
|
||||
WebRemote,
|
||||
} from "./types";
|
||||
import * as T from "./types";
|
||||
import { WSControl } from "./ws";
|
||||
import {
|
||||
measureText,
|
||||
@@ -254,7 +255,7 @@ class Cmd {
|
||||
return webCmd;
|
||||
}
|
||||
|
||||
getExitCode(): boolean {
|
||||
getExitCode(): number {
|
||||
return this.data.get().exitcode;
|
||||
}
|
||||
|
||||
@@ -3606,6 +3607,71 @@ class Model {
|
||||
}
|
||||
return remote.remotecanonicalname;
|
||||
}
|
||||
|
||||
readRemoteFile(screenId: string, lineId: string, path: string): Promise<File> {
|
||||
let urlParams = {
|
||||
screenid: screenId,
|
||||
lineid: lineId,
|
||||
path: path,
|
||||
};
|
||||
let usp = new URLSearchParams(urlParams);
|
||||
let url = new URL(GlobalModel.getBaseHostPort() + "/api/read-file?" + usp.toString());
|
||||
let fetchHeaders = this.getFetchHeaders();
|
||||
let fileInfo: T.FileInfoType = null;
|
||||
let contentType: string = null;
|
||||
let isError = false;
|
||||
let badResponseStr: string = null;
|
||||
let prtn = fetch(url, { method: "get", headers: fetchHeaders })
|
||||
.then((resp) => {
|
||||
if (!resp.ok) {
|
||||
isError = true;
|
||||
badResponseStr = sprintf(
|
||||
"Bad fetch response for /api/read-file: %d %s",
|
||||
resp.status,
|
||||
resp.statusText
|
||||
);
|
||||
return resp.text() as any;
|
||||
}
|
||||
contentType = resp.headers.get("Content-Type");
|
||||
fileInfo = JSON.parse(atob(resp.headers.get("X-FileInfo")));
|
||||
return resp.blob();
|
||||
})
|
||||
.then((blobOrText: any) => {
|
||||
if (blobOrText instanceof Blob) {
|
||||
let blob: Blob = blobOrText;
|
||||
let file = new File([blob], fileInfo.name, { type: blob.type, lastModified: fileInfo.modts });
|
||||
let isWriteable = (fileInfo.perm & 0o222) > 0; // checks for unix permission "w" bits
|
||||
(file as any).readOnly = !isWriteable;
|
||||
return file;
|
||||
} else {
|
||||
let textError: string = blobOrText;
|
||||
if (textError == null || textError.length == 0) {
|
||||
throw new Error(badResponseStr);
|
||||
}
|
||||
throw new Error(textError);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return prtn;
|
||||
}
|
||||
|
||||
writeRemoteFile(screenId: string, lineId: string, path: string, data: Uint8Array, opts?: { useTemp?: boolean }) {
|
||||
opts = opts || {};
|
||||
let params = {
|
||||
screenid: screenId,
|
||||
lineid: lineId,
|
||||
path: path,
|
||||
usetemp: !!opts.useTemp,
|
||||
};
|
||||
let formData = new FormData();
|
||||
formData.append("params", JSON.stringify(params));
|
||||
let blob = new Blob([data], { type: "application/octet-stream" });
|
||||
formData.append("data", blob);
|
||||
let url = new URL(GlobalModel.getBaseHostPort() + "/api/write-file");
|
||||
let fetchHeaders = this.getFetchHeaders();
|
||||
let prtn = fetch(url, { method: "post", headers: fetchHeaders, body: formData });
|
||||
return prtn;
|
||||
}
|
||||
}
|
||||
|
||||
class CommandRunner {
|
||||
@@ -3913,6 +3979,22 @@ class CommandRunner {
|
||||
openSharedSession(): void {
|
||||
GlobalModel.submitCommand("session", "openshared", null, { nohist: "1" }, true);
|
||||
}
|
||||
|
||||
setLineState(
|
||||
screenId: string,
|
||||
lineId: string,
|
||||
state: T.LineStateType,
|
||||
interactive: boolean
|
||||
): Promise<CommandRtnType> {
|
||||
let stateStr = JSON.stringify(state);
|
||||
return GlobalModel.submitCommand(
|
||||
"line",
|
||||
"set",
|
||||
[lineId],
|
||||
{ screen: screenId, nohist: "1", state: stateStr },
|
||||
interactive
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function cmdPacketString(pk: FeCmdPacketType): string {
|
||||
|
||||
+74
-10
@@ -15,12 +15,14 @@ import type {
|
||||
PtyDataType,
|
||||
RendererModel,
|
||||
RendererOptsUpdate,
|
||||
LineStateType,
|
||||
LineType,
|
||||
TermContextUnion,
|
||||
RendererContainerType,
|
||||
} from "./types";
|
||||
import { PacketDataBuffer } from "./ptydata";
|
||||
import { debounce, throttle } from "throttle-debounce";
|
||||
import * as util from "./util";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
type CV<V> = mobx.IComputedValue<V>;
|
||||
@@ -35,8 +37,11 @@ class SimpleBlobRendererModel {
|
||||
loadError: OV<string> = mobx.observable.box(null, {
|
||||
name: "renderer-loadError",
|
||||
});
|
||||
lineState: LineStateType;
|
||||
ptyData: PtyDataType;
|
||||
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
|
||||
dataBlob: Blob;
|
||||
readOnly: boolean;
|
||||
|
||||
initialize(params: RendererModelInitializeParams): void {
|
||||
this.loading = mobx.observable.box(true, { name: "renderer-loading" });
|
||||
@@ -46,6 +51,7 @@ class SimpleBlobRendererModel {
|
||||
this.context = params.context;
|
||||
this.opts = params.opts;
|
||||
this.api = params.api;
|
||||
this.lineState = params.lineState;
|
||||
this.savedHeight = params.savedHeight;
|
||||
this.ptyDataSource = params.ptyDataSource;
|
||||
if (this.isDone.get()) {
|
||||
@@ -86,21 +92,71 @@ class SimpleBlobRendererModel {
|
||||
mobx.action(() => {
|
||||
this.loading.set(true);
|
||||
})();
|
||||
if (delayMs == 0) {
|
||||
this.reload_noDelay();
|
||||
}
|
||||
else {
|
||||
setTimeout(() => {
|
||||
reload_noDelay();
|
||||
}, delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
reload_noDelay(): void {
|
||||
let source = this.lineState["prompt:source"] || "pty";
|
||||
if (source == "pty") {
|
||||
this.reloadPtyData();
|
||||
}
|
||||
else if (source == "file") {
|
||||
this.reloadFileData();
|
||||
}
|
||||
else {
|
||||
mobx.action(() => {
|
||||
this.loadError.set("error: invalid load source: " + source);
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
reloadFileData(): void {
|
||||
// todo add file methods to API, so we don't have a GlobalModel dependency here!
|
||||
let path = this.lineState["prompt:file"];
|
||||
if (util.isBlank(path)) {
|
||||
mobx.action(() => {
|
||||
this.loadError.set("renderer has file source, but no prompt:file specified");
|
||||
})();
|
||||
return;
|
||||
}
|
||||
let rtnp = GlobalModel.readRemoteFile(this.context.screenId, this.context.lineId, path);
|
||||
rtnp.then((file) => {
|
||||
this.readOnly = file.readOnly;
|
||||
this.dataBlob = file;
|
||||
console.log("got file", file);
|
||||
mobx.action(() => {
|
||||
this.loading.set(false);
|
||||
this.loadError.set(null);
|
||||
})();
|
||||
}).catch((e) => {
|
||||
mobx.action(() => {
|
||||
this.loadError.set("error loading file data: " + e);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
reloadPtyData(): void {
|
||||
this.readOnly = true;
|
||||
let rtnp = this.ptyDataSource(this.context);
|
||||
if (rtnp == null) {
|
||||
console.log("no promise returned from ptyDataSource (simplerenderer)", this.context);
|
||||
return;
|
||||
}
|
||||
rtnp.then((ptydata) => {
|
||||
setTimeout(() => {
|
||||
this.ptyData = ptydata;
|
||||
mobx.action(() => {
|
||||
this.loading.set(false);
|
||||
this.loadError.set(null);
|
||||
})();
|
||||
}, delayMs);
|
||||
this.ptyData = ptydata;
|
||||
this.dataBlob = new Blob([this.ptyData.data]);
|
||||
mobx.action(() => {
|
||||
this.loading.set(false);
|
||||
this.loadError.set(null);
|
||||
})();
|
||||
}).catch((e) => {
|
||||
console.log("error loading data", e);
|
||||
mobx.action(() => {
|
||||
this.loadError.set("error loading data: " + e);
|
||||
})();
|
||||
@@ -185,6 +241,14 @@ class SimpleBlobRenderer extends React.Component<
|
||||
render() {
|
||||
let { plugin } = this.props;
|
||||
let model = this.model;
|
||||
if (model.loadError.get() != null) {
|
||||
let height = this.model.savedHeight;
|
||||
return (
|
||||
<div ref={this.wrapperDivRef} style={{ minHeight: height }}>
|
||||
<div className="load-error-text">ERROR: {model.loadError.get()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (model.loading.get()) {
|
||||
let height = this.model.savedHeight;
|
||||
return (
|
||||
@@ -197,7 +261,6 @@ class SimpleBlobRenderer extends React.Component<
|
||||
if (Comp == null) {
|
||||
<div ref={this.wrapperDivRef}>(no component found in plugin)</div>;
|
||||
}
|
||||
let dataBlob = new Blob([model.ptyData.data]);
|
||||
let simpleModel = model as SimpleBlobRendererModel;
|
||||
let { festate, cmdstr } = this.props.initParams.rawCmd;
|
||||
return (
|
||||
@@ -205,7 +268,8 @@ class SimpleBlobRenderer extends React.Component<
|
||||
<Comp
|
||||
cwd={festate.cwd}
|
||||
cmdstr={cmdstr}
|
||||
data={dataBlob}
|
||||
data={simpleModel.dataBlob}
|
||||
lineState={simpleModel.lineState}
|
||||
context={simpleModel.context}
|
||||
opts={simpleModel.opts}
|
||||
savedHeight={simpleModel.savedHeight}
|
||||
|
||||
@@ -23,6 +23,8 @@ type SessionDataType = {
|
||||
full?: boolean;
|
||||
};
|
||||
|
||||
type LineStateType = { [k: string]: any };
|
||||
|
||||
type LineType = {
|
||||
screenid: string;
|
||||
userid: string;
|
||||
@@ -32,6 +34,7 @@ type LineType = {
|
||||
linenumtemp: boolean;
|
||||
linelocal: boolean;
|
||||
linetype: string;
|
||||
linestate: LineStateType;
|
||||
text: string;
|
||||
renderer: string;
|
||||
contentheight?: number;
|
||||
@@ -390,6 +393,7 @@ type RendererModelInitializeParams = {
|
||||
rawCmd: WebCmd;
|
||||
savedHeight: number;
|
||||
opts: RendererOpts;
|
||||
lineState: LineStateType,
|
||||
api: RendererModelContainerApi;
|
||||
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
|
||||
};
|
||||
@@ -600,8 +604,17 @@ type OpenAIPacketType = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
type FileInfoType = {
|
||||
name: string;
|
||||
size: number;
|
||||
modts: number;
|
||||
isdir: boolean;
|
||||
perm: number;
|
||||
};
|
||||
|
||||
export type {
|
||||
SessionDataType,
|
||||
LineStateType,
|
||||
LineType,
|
||||
RemoteType,
|
||||
RemoteStateType,
|
||||
@@ -667,4 +680,5 @@ export type {
|
||||
RemoteViewType,
|
||||
CommandRtnType,
|
||||
OpenAIPacketType,
|
||||
FileInfoType,
|
||||
};
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import * as mobx from "mobx";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { RendererContext, RendererOpts } from "../types";
|
||||
import { RendererContext, RendererOpts, LineStateType } from "../types";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { GlobalModel } from "../model";
|
||||
|
||||
@@ -16,6 +16,7 @@ class SourceCodeRenderer extends React.Component<
|
||||
context: RendererContext;
|
||||
opts: RendererOpts;
|
||||
savedHeight: number;
|
||||
lineState: LineStateType;
|
||||
},
|
||||
{}
|
||||
> {
|
||||
|
||||
Reference in New Issue
Block a user