fix stream-file urls to use new remoteuri (#1984)

This commit is contained in:
Mike Sawka 2025-02-17 17:55:57 -08:00 committed by GitHub
parent 1929c70e04
commit e15d38a795
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 92 additions and 64 deletions

View File

@ -87,7 +87,10 @@ export function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWill
} }
if ( if (
event.frame.name == "pdfview" && event.frame.name == "pdfview" &&
(url.startsWith("blob:file:///") || url.startsWith(getWebServerEndpoint() + "/wave/stream-file?")) (url.startsWith("blob:file:///") ||
url.startsWith(getWebServerEndpoint() + "/wave/stream-file?") ||
url.startsWith(getWebServerEndpoint() + "/wave/stream-file/") ||
url.startsWith(getWebServerEndpoint() + "/wave/stream-local-file?"))
) { ) {
// allowed // allowed
return; return;

View File

@ -240,7 +240,9 @@ electron.ipcMain.on("webview-image-contextmenu", (event: electron.IpcMainEvent,
}); });
electron.ipcMain.on("download", (event, payload) => { electron.ipcMain.on("download", (event, payload) => {
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(payload.filePath); const baseName = encodeURIComponent(path.basename(payload.filePath));
const streamingUrl =
getWebServerEndpoint() + "/wave/stream-file/" + baseName + "?path=" + encodeURIComponent(payload.filePath);
event.sender.downloadURL(streamingUrl); event.sender.downloadURL(streamingUrl);
}); });

View File

@ -241,6 +241,7 @@ const BlockFrame_Header = ({
icon: "link-slash", icon: "link-slash",
title: "wsh is not installed for this connection", title: "wsh is not installed for this connection",
}; };
const showNoWshButton = manageConnection && wshProblem && !util.isBlank(connName) && !connName.startsWith("aws:");
return ( return (
<div <div
@ -263,9 +264,7 @@ const BlockFrame_Header = ({
changeConnModalAtom={changeConnModalAtom} changeConnModalAtom={changeConnModalAtom}
/> />
)} )}
{manageConnection && wshProblem && ( {showNoWshButton && <IconButton decl={wshInstallButton} className="block-frame-header-iconbutton" />}
<IconButton decl={wshInstallButton} className="block-frame-header-iconbutton" />
)}
<div className="block-frame-textelems-wrapper">{headerTextElems}</div> <div className="block-frame-textelems-wrapper">{headerTextElems}</div>
<div className="block-frame-end-icons">{endIconsElem}</div> <div className="block-frame-end-icons">{endIconsElem}</div>
</div> </div>

View File

@ -4,7 +4,7 @@
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil"; import { TabRpcClient } from "@/app/store/wshrpcutil";
import { getWebServerEndpoint } from "@/util/endpoints"; import { getWebServerEndpoint } from "@/util/endpoints";
import { isBlank, makeConnRoute } from "@/util/util"; import { formatRemoteUri } from "@/util/waveutil";
import parseSrcSet from "parse-srcset"; import parseSrcSet from "parse-srcset";
export type MarkdownContentBlockType = { export type MarkdownContentBlockType = {
@ -158,19 +158,13 @@ export const resolveRemoteFile = async (filepath: string, resolveOpts: MarkdownR
if (!filepath || filepath.startsWith("http://") || filepath.startsWith("https://")) { if (!filepath || filepath.startsWith("http://") || filepath.startsWith("https://")) {
return filepath; return filepath;
} }
try { try {
const route = makeConnRoute(resolveOpts.connName); const baseDirUri = formatRemoteUri(resolveOpts.baseDir, resolveOpts.connName);
const fileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [resolveOpts.baseDir, filepath], { const fileInfo = await RpcApi.FileJoinCommand(TabRpcClient, [baseDirUri, filepath]);
route: route, const remoteUri = formatRemoteUri(fileInfo.path, resolveOpts.connName);
}); console.log("markdown resolve", resolveOpts, filepath, "=>", baseDirUri, remoteUri);
const usp = new URLSearchParams(); const usp = new URLSearchParams();
usp.set("path", fileInfo.path); usp.set("path", remoteUri);
if (!isBlank(resolveOpts.connName)) {
usp.set("connection", resolveOpts.connName);
}
return getWebServerEndpoint() + "/wave/stream-file?" + usp.toString(); return getWebServerEndpoint() + "/wave/stream-file?" + usp.toString();
} catch (err) { } catch (err) {
console.warn("Failed to resolve remote file:", filepath, err); console.warn("Failed to resolve remote file:", filepath, err);

View File

@ -348,6 +348,7 @@ const ChangeConnectionBlockModal = React.memo(
const connStatusMap = new Map<string, ConnStatus>(); const connStatusMap = new Map<string, ConnStatus>();
const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom); const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom);
let filterOutNowsh = util.useAtomValueSafe(viewModel.filterOutNowsh) ?? true; let filterOutNowsh = util.useAtomValueSafe(viewModel.filterOutNowsh) ?? true;
const showS3 = util.useAtomValueSafe(viewModel.showS3) ?? false;
let maxActiveConnNum = 1; let maxActiveConnNum = 1;
for (const conn of allConnStatus) { for (const conn of allConnStatus) {
@ -436,14 +437,17 @@ const ChangeConnectionBlockModal = React.memo(
fullConfig, fullConfig,
filterOutNowsh filterOutNowsh
); );
const s3Suggestions = getS3Suggestions( let s3Suggestions: SuggestionConnectionScope = null;
s3List, if (showS3) {
connection, s3Suggestions = getS3Suggestions(
connSelected, s3List,
connStatusMap, connection,
fullConfig, connSelected,
filterOutNowsh connStatusMap,
); fullConfig,
filterOutNowsh
);
}
const connectionsEditItem = getConnectionsEditItem(changeConnModalAtom, connSelected); const connectionsEditItem = getConnectionsEditItem(changeConnModalAtom, connSelected);
const disconnectItem = getDisconnectItem(connection, connStatusMap); const disconnectItem = getDisconnectItem(connection, connStatusMap);
const newConnectionSuggestionItem = getNewConnectionSuggestionItem( const newConnectionSuggestionItem = getNewConnectionSuggestionItem(

View File

@ -9,9 +9,10 @@ import { ContextMenuModel } from "@/app/store/contextmenu";
import { PLATFORM, atoms, createBlock, getApi, globalStore } from "@/app/store/global"; import { PLATFORM, atoms, createBlock, getApi, globalStore } from "@/app/store/global";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil"; import { TabRpcClient } from "@/app/store/wshrpcutil";
import { formatRemoteUri, type PreviewModel } from "@/app/view/preview/preview"; import { type PreviewModel } from "@/app/view/preview/preview";
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil"; import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
import { fireAndForget, isBlank, makeNativeLabel } from "@/util/util"; import { fireAndForget, isBlank, makeNativeLabel } from "@/util/util";
import { formatRemoteUri } from "@/util/waveutil";
import { offset, useDismiss, useFloating, useInteractions } from "@floating-ui/react"; import { offset, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
import { import {
Column, Column,
@ -575,7 +576,8 @@ function TableBody({
{ {
label: "Download File", label: "Download File",
click: () => { click: () => {
getApi().downloadFile(normPath); const remoteUri = formatRemoteUri(finfo.path, conn);
getApi().downloadFile(remoteUri);
}, },
}, },
{ {

View File

@ -38,6 +38,7 @@ import {
makeNativeLabel, makeNativeLabel,
stringToBase64, stringToBase64,
} from "@/util/util"; } from "@/util/util";
import { formatRemoteUri } from "@/util/waveutil";
import { Monaco } from "@monaco-editor/react"; import { Monaco } from "@monaco-editor/react";
import clsx from "clsx"; import clsx from "clsx";
import { Atom, atom, Getter, PrimitiveAtom, useAtom, useAtomValue, useSetAtom, WritableAtom } from "jotai"; import { Atom, atom, Getter, PrimitiveAtom, useAtom, useAtomValue, useSetAtom, WritableAtom } from "jotai";
@ -180,6 +181,8 @@ export class PreviewModel implements ViewModel {
directoryKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean; directoryKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean;
codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean; codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean;
showS3 = atom(true);
constructor(blockId: string, nodeModel: BlockNodeModel) { constructor(blockId: string, nodeModel: BlockNodeModel) {
this.viewType = "preview"; this.viewType = "preview";
this.blockId = blockId; this.blockId = blockId;
@ -936,13 +939,14 @@ function StreamingPreview({ model }: SpecializedViewProps) {
const conn = useAtomValue(model.connection); const conn = useAtomValue(model.connection);
const fileInfo = useAtomValue(model.statFile); const fileInfo = useAtomValue(model.statFile);
const filePath = fileInfo.path; const filePath = fileInfo.path;
const remotePath = formatRemoteUri(filePath, conn);
const usp = new URLSearchParams(); const usp = new URLSearchParams();
usp.set("path", filePath); usp.set("path", remotePath);
if (conn != null) { if (conn != null) {
usp.set("connection", conn); usp.set("connection", conn);
} }
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?" + usp.toString(); const streamingUrl = `${getWebServerEndpoint()}/wave/stream-file?${usp.toString()}`;
if (fileInfo.mimetype == "application/pdf") { if (fileInfo.mimetype === "application/pdf") {
return ( return (
<div className="view-preview view-preview-pdf"> <div className="view-preview view-preview-pdf">
<iframe src={streamingUrl} width="100%" height="100%" name="pdfview" /> <iframe src={streamingUrl} width="100%" height="100%" name="pdfview" />
@ -1304,16 +1308,4 @@ const ErrorOverlay = memo(({ errorMsg, resetOverlay }: { errorMsg: ErrorMsg; res
); );
}); });
function formatRemoteUri(path: string, connection: string): string { export { PreviewView };
connection = connection ?? "local";
// TODO: We need a better way to handle s3 paths
let retVal: string;
if (connection.startsWith("aws:")) {
retVal = `${connection}:s3://${path ?? ""}`;
} else {
retVal = `wsh://${connection}/${path}`;
}
return retVal;
}
export { formatRemoteUri, PreviewView };

View File

@ -119,7 +119,8 @@ function TermSticker({ sticker, config }: { sticker: StickerType; config: Sticke
if (sticker.imgsrc == null) { if (sticker.imgsrc == null) {
return null; return null;
} }
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(sticker.imgsrc); const streamingUrl =
getWebServerEndpoint() + "/wave/stream-local-file?path=" + encodeURIComponent(sticker.imgsrc);
return ( return (
<div className="term-sticker term-sticker-image" style={style} onClick={clickHandler}> <div className="term-sticker term-sticker-image" style={style} onClick={clickHandler}>
<img src={streamingUrl} /> <img src={streamingUrl} />

View File

@ -292,6 +292,9 @@ declare global {
// If true, filters out 'nowsh' connections (when managing connections) // If true, filters out 'nowsh' connections (when managing connections)
filterOutNowsh?: jotai.Atom<boolean>; filterOutNowsh?: jotai.Atom<boolean>;
// if true, show s3 connections in picker
showS3?: jotai.Atom<boolean>;
// If true, removes padding inside the block content area. // If true, removes padding inside the block content area.
noPadding?: jotai.Atom<boolean>; noPadding?: jotai.Atom<boolean>;

View File

@ -7,7 +7,9 @@ import { generate as generateCSS, parse as parseCSS, walk as walkCSS } from "css
function encodeFileURL(file: string) { function encodeFileURL(file: string) {
const webEndpoint = getWebServerEndpoint(); const webEndpoint = getWebServerEndpoint();
return webEndpoint + `/wave/stream-file?path=${encodeURIComponent(file)}&no404=1`; const fileUri = formatRemoteUri(file, "local");
const rtn = webEndpoint + `/wave/stream-file?path=${encodeURIComponent(fileUri)}&no404=1`;
return rtn;
} }
export function processBackgroundUrls(cssText: string): string { export function processBackgroundUrls(cssText: string): string {
@ -86,3 +88,15 @@ export function computeBgStyleFromMeta(meta: MetaType, defaultOpacity: number =
return null; return null;
} }
} }
export function formatRemoteUri(path: string, connection: string): string {
connection = connection ?? "local";
// TODO: We need a better way to handle s3 paths
let retVal: string;
if (connection.startsWith("aws:")) {
retVal = `${connection}:s3://${path ?? ""}`;
} else {
retVal = `wsh://${connection}/${path}`;
}
return retVal;
}

View File

@ -774,20 +774,13 @@ func (c S3Client) Delete(ctx context.Context, conn *connparse.Connection, recurs
func (c S3Client) Join(ctx context.Context, conn *connparse.Connection, parts ...string) (*wshrpc.FileInfo, error) { func (c S3Client) Join(ctx context.Context, conn *connparse.Connection, parts ...string) (*wshrpc.FileInfo, error) {
var joinParts []string var joinParts []string
if conn.Host == "" || conn.Host == fspath.Separator { if conn.Path == "" || conn.Path == fspath.Separator {
if conn.Path == "" || conn.Path == fspath.Separator { joinParts = parts
joinParts = parts
} else {
joinParts = append([]string{conn.Path}, parts...)
}
} else if conn.Path == "" || conn.Path == "/" {
joinParts = append([]string{conn.Host}, parts...)
} else { } else {
joinParts = append([]string{conn.Host, conn.Path}, parts...) joinParts = append([]string{conn.Path}, parts...)
} }
conn.Path = fspath.Join(joinParts...) conn.Path = fspath.Join(joinParts...)
return c.Stat(ctx, conn) return c.Stat(ctx, conn)
} }

View File

@ -25,6 +25,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/docsite" "github.com/wavetermdev/waveterm/pkg/docsite"
"github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/remote/fileshare"
"github.com/wavetermdev/waveterm/pkg/schema" "github.com/wavetermdev/waveterm/pkg/schema"
"github.com/wavetermdev/waveterm/pkg/service" "github.com/wavetermdev/waveterm/pkg/service"
"github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/util/utilfn"
@ -251,6 +252,10 @@ func handleRemoteStreamFile(w http.ResponseWriter, req *http.Request, conn strin
route := wshutil.MakeConnectionRouteId(conn) route := wshutil.MakeConnectionRouteId(conn)
rpcOpts := &wshrpc.RpcOpts{Route: route, Timeout: 60 * 1000} rpcOpts := &wshrpc.RpcOpts{Route: route, Timeout: 60 * 1000}
rtnCh := wshclient.RemoteStreamFileCommand(client, streamFileData, rpcOpts) rtnCh := wshclient.RemoteStreamFileCommand(client, streamFileData, rpcOpts)
return handleRemoteStreamFileFromCh(w, req, path, rtnCh, rpcOpts.StreamCancelFn, no404)
}
func handleRemoteStreamFileFromCh(w http.ResponseWriter, req *http.Request, path string, rtnCh <-chan wshrpc.RespOrErrorUnion[wshrpc.FileData], streamCancelFn func(), no404 bool) error {
firstPk := true firstPk := true
var fileInfo *wshrpc.FileInfo var fileInfo *wshrpc.FileInfo
loopDone := false loopDone := false
@ -265,7 +270,9 @@ func handleRemoteStreamFile(w http.ResponseWriter, req *http.Request, conn strin
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
rpcOpts.StreamCancelFn() if streamCancelFn != nil {
streamCancelFn()
}
return ctx.Err() return ctx.Err()
case respUnion, ok := <-rtnCh: case respUnion, ok := <-rtnCh:
if !ok { if !ok {
@ -311,6 +318,16 @@ func handleRemoteStreamFile(w http.ResponseWriter, req *http.Request, conn strin
} }
} }
func handleStreamLocalFile(w http.ResponseWriter, r *http.Request) {
path := r.URL.Query().Get("path")
if path == "" {
http.Error(w, "path is required", http.StatusBadRequest)
return
}
no404 := r.URL.Query().Get("no404")
handleLocalStreamFile(w, r, path, no404 != "")
}
func handleStreamFile(w http.ResponseWriter, r *http.Request) { func handleStreamFile(w http.ResponseWriter, r *http.Request) {
conn := r.URL.Query().Get("connection") conn := r.URL.Query().Get("connection")
if conn == "" { if conn == "" {
@ -322,14 +339,16 @@ func handleStreamFile(w http.ResponseWriter, r *http.Request) {
return return
} }
no404 := r.URL.Query().Get("no404") no404 := r.URL.Query().Get("no404")
if conn == wshrpc.LocalConnName { data := wshrpc.FileData{
handleLocalStreamFile(w, r, path, no404 != "") Info: &wshrpc.FileInfo{
} else { Path: path,
err := handleRemoteStreamFile(w, r, conn, path, no404 != "") },
if err != nil { }
log.Printf("error streaming remote file %q %q: %v\n", conn, path, err) rtnCh := fileshare.ReadStream(r.Context(), data)
http.Error(w, fmt.Sprintf("error streaming file: %v", err), http.StatusInternalServerError) err := handleRemoteStreamFileFromCh(w, r, path, rtnCh, nil, no404 != "")
} if err != nil {
log.Printf("error streaming file %q %q: %v\n", conn, path, err)
http.Error(w, fmt.Sprintf("error streaming file: %v", err), http.StatusInternalServerError)
} }
} }
@ -423,7 +442,9 @@ const schemaPrefix = "/schema/"
// blocking // blocking
func RunWebServer(listener net.Listener) { func RunWebServer(listener net.Listener) {
gr := mux.NewRouter() gr := mux.NewRouter()
gr.HandleFunc("/wave/stream-local-file", WebFnWrap(WebFnOpts{AllowCaching: true}, handleStreamLocalFile))
gr.HandleFunc("/wave/stream-file", WebFnWrap(WebFnOpts{AllowCaching: true}, handleStreamFile)) gr.HandleFunc("/wave/stream-file", WebFnWrap(WebFnOpts{AllowCaching: true}, handleStreamFile))
gr.PathPrefix("/wave/stream-file/").HandlerFunc(WebFnWrap(WebFnOpts{AllowCaching: true}, handleStreamFile))
gr.HandleFunc("/wave/file", WebFnWrap(WebFnOpts{AllowCaching: false}, handleWaveFile)) gr.HandleFunc("/wave/file", WebFnWrap(WebFnOpts{AllowCaching: false}, handleWaveFile))
gr.HandleFunc("/wave/service", WebFnWrap(WebFnOpts{JsonErrors: true}, handleService)) gr.HandleFunc("/wave/service", WebFnWrap(WebFnOpts{JsonErrors: true}, handleService))
gr.HandleFunc("/vdom/{uuid}/{path:.*}", WebFnWrap(WebFnOpts{AllowCaching: true}, handleVDom)) gr.HandleFunc("/vdom/{uuid}/{path:.*}", WebFnWrap(WebFnOpts{AllowCaching: true}, handleVDom))