From c9283cac70479313998fc68122882a1c3f5bc432 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Fri, 10 Apr 2026 18:05:13 +0600 Subject: [PATCH 1/5] Add filesystem operations (list, mkdir, remove) across full stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plumb ListDir, MakeDir, and RemovePath through all layers: REST API → host agent RPC → envdclient → envd. These endpoints enable a web file browser for sandbox filesystem interaction. New endpoints (all under requireAPIKeyOrJWT): - POST /v1/sandboxes/{id}/files/list - POST /v1/sandboxes/{id}/files/mkdir - POST /v1/sandboxes/{id}/files/remove --- internal/api/handlers_fs.go | 236 +++++++ internal/api/openapi.yaml | 188 +++++ internal/api/server.go | 4 + internal/envdclient/client.go | 27 + internal/hostagent/server.go | 104 +++ proto/hostagent/gen/hostagent.pb.go | 647 +++++++++++++++--- .../hostagentv1connect/hostagent.connect.go | 93 +++ proto/hostagent/hostagent.proto | 53 ++ 8 files changed, 1258 insertions(+), 94 deletions(-) create mode 100644 internal/api/handlers_fs.go diff --git a/internal/api/handlers_fs.go b/internal/api/handlers_fs.go new file mode 100644 index 0000000..30073c3 --- /dev/null +++ b/internal/api/handlers_fs.go @@ -0,0 +1,236 @@ +package api + +import ( + "net/http" + + "connectrpc.com/connect" + "github.com/go-chi/chi/v5" + + "git.omukk.dev/wrenn/wrenn/internal/auth" + "git.omukk.dev/wrenn/wrenn/internal/db" + "git.omukk.dev/wrenn/wrenn/internal/id" + "git.omukk.dev/wrenn/wrenn/internal/lifecycle" + pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen" +) + +type fsHandler struct { + db *db.Queries + pool *lifecycle.HostClientPool +} + +func newFSHandler(db *db.Queries, pool *lifecycle.HostClientPool) *fsHandler { + return &fsHandler{db: db, pool: pool} +} + +type listDirRequest struct { + Path string `json:"path"` + Depth uint32 `json:"depth"` +} + +type fileEntryResponse struct { + Name string `json:"name"` + Path string `json:"path"` + Type string `json:"type"` + Size int64 `json:"size"` + Mode uint32 `json:"mode"` + Permissions string `json:"permissions"` + Owner string `json:"owner"` + Group string `json:"group"` + ModifiedAt int64 `json:"modified_at"` + SymlinkTarget *string `json:"symlink_target,omitempty"` +} + +type listDirResponse struct { + Entries []fileEntryResponse `json:"entries"` +} + +type makeDirRequest struct { + Path string `json:"path"` +} + +type makeDirResponse struct { + Entry fileEntryResponse `json:"entry"` +} + +type removeRequest struct { + Path string `json:"path"` +} + +// ListDir handles POST /v1/sandboxes/{id}/files/list. +func (h *fsHandler) ListDir(w http.ResponseWriter, r *http.Request) { + sandboxIDStr := chi.URLParam(r, "id") + ctx := r.Context() + ac := auth.MustFromContext(ctx) + + sandboxID, err := id.ParseSandboxID(sandboxIDStr) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID") + return + } + + sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID}) + if err != nil { + writeError(w, http.StatusNotFound, "not_found", "sandbox not found") + return + } + if sb.Status != "running" { + writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running") + return + } + + var req listDirRequest + if err := decodeJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "invalid_request", "invalid JSON body") + return + } + if req.Path == "" { + writeError(w, http.StatusBadRequest, "invalid_request", "path is required") + return + } + + agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID) + if err != nil { + writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable") + return + } + + resp, err := agent.ListDir(ctx, connect.NewRequest(&pb.ListDirRequest{ + SandboxId: sandboxIDStr, + Path: req.Path, + Depth: req.Depth, + })) + if err != nil { + status, code, msg := agentErrToHTTP(err) + writeError(w, status, code, msg) + return + } + + entries := make([]fileEntryResponse, 0, len(resp.Msg.Entries)) + for _, e := range resp.Msg.Entries { + entries = append(entries, fileEntryFromPB(e)) + } + + writeJSON(w, http.StatusOK, listDirResponse{Entries: entries}) +} + +// MakeDir handles POST /v1/sandboxes/{id}/files/mkdir. +func (h *fsHandler) MakeDir(w http.ResponseWriter, r *http.Request) { + sandboxIDStr := chi.URLParam(r, "id") + ctx := r.Context() + ac := auth.MustFromContext(ctx) + + sandboxID, err := id.ParseSandboxID(sandboxIDStr) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID") + return + } + + sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID}) + if err != nil { + writeError(w, http.StatusNotFound, "not_found", "sandbox not found") + return + } + if sb.Status != "running" { + writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running") + return + } + + var req makeDirRequest + if err := decodeJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "invalid_request", "invalid JSON body") + return + } + if req.Path == "" { + writeError(w, http.StatusBadRequest, "invalid_request", "path is required") + return + } + + agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID) + if err != nil { + writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable") + return + } + + resp, err := agent.MakeDir(ctx, connect.NewRequest(&pb.MakeDirRequest{ + SandboxId: sandboxIDStr, + Path: req.Path, + })) + if err != nil { + status, code, msg := agentErrToHTTP(err) + writeError(w, status, code, msg) + return + } + + writeJSON(w, http.StatusOK, makeDirResponse{Entry: fileEntryFromPB(resp.Msg.Entry)}) +} + +// Remove handles POST /v1/sandboxes/{id}/files/remove. +func (h *fsHandler) Remove(w http.ResponseWriter, r *http.Request) { + sandboxIDStr := chi.URLParam(r, "id") + ctx := r.Context() + ac := auth.MustFromContext(ctx) + + sandboxID, err := id.ParseSandboxID(sandboxIDStr) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID") + return + } + + sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID}) + if err != nil { + writeError(w, http.StatusNotFound, "not_found", "sandbox not found") + return + } + if sb.Status != "running" { + writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running") + return + } + + var req removeRequest + if err := decodeJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "invalid_request", "invalid JSON body") + return + } + if req.Path == "" { + writeError(w, http.StatusBadRequest, "invalid_request", "path is required") + return + } + + agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID) + if err != nil { + writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable") + return + } + + if _, err := agent.RemovePath(ctx, connect.NewRequest(&pb.RemovePathRequest{ + SandboxId: sandboxIDStr, + Path: req.Path, + })); err != nil { + status, code, msg := agentErrToHTTP(err) + writeError(w, status, code, msg) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func fileEntryFromPB(e *pb.FileEntry) fileEntryResponse { + if e == nil { + return fileEntryResponse{} + } + resp := fileEntryResponse{ + Name: e.Name, + Path: e.Path, + Type: e.Type, + Size: e.Size, + Mode: e.Mode, + Permissions: e.Permissions, + Owner: e.Owner, + Group: e.Group, + ModifiedAt: e.ModifiedAt, + } + if e.SymlinkTarget != nil { + resp.SymlinkTarget = e.SymlinkTarget + } + return resp +} diff --git a/internal/api/openapi.yaml b/internal/api/openapi.yaml index 0b4fe74..82271f2 100644 --- a/internal/api/openapi.yaml +++ b/internal/api/openapi.yaml @@ -1037,6 +1037,122 @@ paths: schema: $ref: "#/components/schemas/Error" + /v1/sandboxes/{id}/files/list: + parameters: + - name: id + in: path + required: true + schema: + type: string + + post: + summary: List directory contents + operationId: listDir + tags: [sandboxes] + security: + - apiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ListDirRequest" + responses: + "200": + description: Directory listing + content: + application/json: + schema: + $ref: "#/components/schemas/ListDirResponse" + "404": + description: Sandbox not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "409": + description: Sandbox not running + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /v1/sandboxes/{id}/files/mkdir: + parameters: + - name: id + in: path + required: true + schema: + type: string + + post: + summary: Create a directory + operationId: makeDir + tags: [sandboxes] + security: + - apiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/MakeDirRequest" + responses: + "200": + description: Directory created + content: + application/json: + schema: + $ref: "#/components/schemas/MakeDirResponse" + "404": + description: Sandbox not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "409": + description: Sandbox not running + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /v1/sandboxes/{id}/files/remove: + parameters: + - name: id + in: path + required: true + schema: + type: string + + post: + summary: Remove a file or directory + operationId: removePath + tags: [sandboxes] + security: + - apiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RemoveRequest" + responses: + "204": + description: File or directory removed + "404": + description: Sandbox not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "409": + description: Sandbox not running + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /v1/sandboxes/{id}/exec/stream: parameters: - name: id @@ -1988,6 +2104,78 @@ components: type: string description: Absolute file path inside the sandbox + ListDirRequest: + type: object + required: [path] + properties: + path: + type: string + description: Directory path inside the sandbox + depth: + type: integer + default: 1 + description: Recursion depth (0 = non-recursive, 1 = immediate children) + + ListDirResponse: + type: object + properties: + entries: + type: array + items: + $ref: "#/components/schemas/FileEntry" + + FileEntry: + type: object + properties: + name: + type: string + path: + type: string + type: + type: string + enum: [file, directory, symlink] + size: + type: integer + format: int64 + mode: + type: integer + permissions: + type: string + description: Human-readable permissions (e.g. "-rwxr-xr-x") + owner: + type: string + group: + type: string + modified_at: + type: integer + format: int64 + description: Unix timestamp (seconds) + symlink_target: + type: string + nullable: true + + MakeDirRequest: + type: object + required: [path] + properties: + path: + type: string + description: Directory path to create inside the sandbox + + MakeDirResponse: + type: object + properties: + entry: + $ref: "#/components/schemas/FileEntry" + + RemoveRequest: + type: object + required: [path] + properties: + path: + type: string + description: Path to remove inside the sandbox + CreateHostRequest: type: object required: [type] diff --git a/internal/api/server.go b/internal/api/server.go index aeb3625..1f4b208 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -60,6 +60,7 @@ func New( execStream := newExecStreamHandler(queries, pool) files := newFilesHandler(queries, pool) filesStream := newFilesStreamHandler(queries, pool) + fsH := newFSHandler(queries, pool) snapshots := newSnapshotHandler(templateSvc, queries, pool, al) authH := newAuthHandler(queries, pgPool, jwtSecret) oauthH := newOAuthHandler(queries, pgPool, jwtSecret, oauthRegistry, oauthRedirectURL) @@ -133,6 +134,9 @@ func New( r.Post("/files/read", files.Download) r.Post("/files/stream/write", filesStream.StreamUpload) r.Post("/files/stream/read", filesStream.StreamDownload) + r.Post("/files/list", fsH.ListDir) + r.Post("/files/mkdir", fsH.MakeDir) + r.Post("/files/remove", fsH.Remove) r.Get("/metrics", metricsH.GetMetrics) }) }) diff --git a/internal/envdclient/client.go b/internal/envdclient/client.go index 3e05f7d..d20ffd5 100644 --- a/internal/envdclient/client.go +++ b/internal/envdclient/client.go @@ -282,3 +282,30 @@ func (c *Client) ListDir(ctx context.Context, path string, depth uint32) (*envdp return resp.Msg, nil } + +// MakeDir creates a directory inside the sandbox. +func (c *Client) MakeDir(ctx context.Context, path string) (*envdpb.MakeDirResponse, error) { + req := connect.NewRequest(&envdpb.MakeDirRequest{ + Path: path, + }) + + resp, err := c.filesystem.MakeDir(ctx, req) + if err != nil { + return nil, fmt.Errorf("make dir: %w", err) + } + + return resp.Msg, nil +} + +// Remove removes a file or directory inside the sandbox. +func (c *Client) Remove(ctx context.Context, path string) error { + req := connect.NewRequest(&envdpb.RemoveRequest{ + Path: path, + }) + + if _, err := c.filesystem.Remove(ctx, req); err != nil { + return fmt.Errorf("remove: %w", err) + } + + return nil +} diff --git a/internal/hostagent/server.go b/internal/hostagent/server.go index f6a5522..bd52fe0 100644 --- a/internal/hostagent/server.go +++ b/internal/hostagent/server.go @@ -15,6 +15,7 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" + envdpb "git.omukk.dev/wrenn/wrenn/proto/envd/gen" pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen" "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect" @@ -252,6 +253,69 @@ func (s *Server) ReadFile( return connect.NewResponse(&pb.ReadFileResponse{Content: content}), nil } +func (s *Server) ListDir( + ctx context.Context, + req *connect.Request[pb.ListDirRequest], +) (*connect.Response[pb.ListDirResponse], error) { + msg := req.Msg + + client, err := s.mgr.GetClient(msg.SandboxId) + if err != nil { + return nil, connect.NewError(connect.CodeNotFound, err) + } + + resp, err := client.ListDir(ctx, msg.Path, msg.Depth) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("list dir: %w", err)) + } + + entries := make([]*pb.FileEntry, 0, len(resp.Entries)) + for _, e := range resp.Entries { + entries = append(entries, entryInfoToPB(e)) + } + + return connect.NewResponse(&pb.ListDirResponse{Entries: entries}), nil +} + +func (s *Server) MakeDir( + ctx context.Context, + req *connect.Request[pb.MakeDirRequest], +) (*connect.Response[pb.MakeDirResponse], error) { + msg := req.Msg + + client, err := s.mgr.GetClient(msg.SandboxId) + if err != nil { + return nil, connect.NewError(connect.CodeNotFound, err) + } + + resp, err := client.MakeDir(ctx, msg.Path) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("make dir: %w", err)) + } + + return connect.NewResponse(&pb.MakeDirResponse{ + Entry: entryInfoToPB(resp.Entry), + }), nil +} + +func (s *Server) RemovePath( + ctx context.Context, + req *connect.Request[pb.RemovePathRequest], +) (*connect.Response[pb.RemovePathResponse], error) { + msg := req.Msg + + client, err := s.mgr.GetClient(msg.SandboxId) + if err != nil { + return nil, connect.NewError(connect.CodeNotFound, err) + } + + if err := client.Remove(ctx, msg.Path); err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("remove: %w", err)) + } + + return connect.NewResponse(&pb.RemovePathResponse{}), nil +} + func (s *Server) ExecStream( ctx context.Context, req *connect.Request[pb.ExecStreamRequest], @@ -545,3 +609,43 @@ func metricPointsToPB(pts []sandbox.MetricPoint) []*pb.MetricPoint { } return out } + +// entryInfoToPB maps an envd EntryInfo to a hostagent FileEntry. +func entryInfoToPB(e *envdpb.EntryInfo) *pb.FileEntry { + if e == nil { + return nil + } + + var fileType string + switch e.Type { + case envdpb.FileType_FILE_TYPE_FILE: + fileType = "file" + case envdpb.FileType_FILE_TYPE_DIRECTORY: + fileType = "directory" + case envdpb.FileType_FILE_TYPE_SYMLINK: + fileType = "symlink" + default: + fileType = "unknown" + } + + entry := &pb.FileEntry{ + Name: e.Name, + Path: e.Path, + Type: fileType, + Size: e.Size, + Mode: e.Mode, + Permissions: e.Permissions, + Owner: e.Owner, + Group: e.Group, + } + + if e.ModifiedTime != nil { + entry.ModifiedAt = e.ModifiedTime.GetSeconds() + } + + if e.SymlinkTarget != nil { + entry.SymlinkTarget = e.SymlinkTarget + } + + return entry +} diff --git a/proto/hostagent/gen/hostagent.pb.go b/proto/hostagent/gen/hostagent.pb.go index b4ab783..e2b783e 100644 --- a/proto/hostagent/gen/hostagent.pb.go +++ b/proto/hostagent/gen/hostagent.pb.go @@ -1833,6 +1833,413 @@ func (x *ReadFileStreamResponse) GetChunk() []byte { return nil } +type ListDirRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Depth uint32 `protobuf:"varint,3,opt,name=depth,proto3" json:"depth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListDirRequest) Reset() { + *x = ListDirRequest{} + mi := &file_hostagent_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListDirRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListDirRequest) ProtoMessage() {} + +func (x *ListDirRequest) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListDirRequest.ProtoReflect.Descriptor instead. +func (*ListDirRequest) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{31} +} + +func (x *ListDirRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ListDirRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *ListDirRequest) GetDepth() uint32 { + if x != nil { + return x.Depth + } + return 0 +} + +type ListDirResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entries []*FileEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListDirResponse) Reset() { + *x = ListDirResponse{} + mi := &file_hostagent_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListDirResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListDirResponse) ProtoMessage() {} + +func (x *ListDirResponse) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListDirResponse.ProtoReflect.Descriptor instead. +func (*ListDirResponse) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{32} +} + +func (x *ListDirResponse) GetEntries() []*FileEntry { + if x != nil { + return x.Entries + } + return nil +} + +type FileEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + // "file", "directory", or "symlink". + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + Mode uint32 `protobuf:"varint,5,opt,name=mode,proto3" json:"mode,omitempty"` + // Human-readable permissions string, e.g. "-rwxr-xr-x". + Permissions string `protobuf:"bytes,6,opt,name=permissions,proto3" json:"permissions,omitempty"` + Owner string `protobuf:"bytes,7,opt,name=owner,proto3" json:"owner,omitempty"` + Group string `protobuf:"bytes,8,opt,name=group,proto3" json:"group,omitempty"` + // Last modification time as Unix timestamp (seconds). + ModifiedAt int64 `protobuf:"varint,9,opt,name=modified_at,json=modifiedAt,proto3" json:"modified_at,omitempty"` + SymlinkTarget *string `protobuf:"bytes,10,opt,name=symlink_target,json=symlinkTarget,proto3,oneof" json:"symlink_target,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FileEntry) Reset() { + *x = FileEntry{} + mi := &file_hostagent_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FileEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FileEntry) ProtoMessage() {} + +func (x *FileEntry) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FileEntry.ProtoReflect.Descriptor instead. +func (*FileEntry) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{33} +} + +func (x *FileEntry) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *FileEntry) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *FileEntry) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *FileEntry) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *FileEntry) GetMode() uint32 { + if x != nil { + return x.Mode + } + return 0 +} + +func (x *FileEntry) GetPermissions() string { + if x != nil { + return x.Permissions + } + return "" +} + +func (x *FileEntry) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *FileEntry) GetGroup() string { + if x != nil { + return x.Group + } + return "" +} + +func (x *FileEntry) GetModifiedAt() int64 { + if x != nil { + return x.ModifiedAt + } + return 0 +} + +func (x *FileEntry) GetSymlinkTarget() string { + if x != nil && x.SymlinkTarget != nil { + return *x.SymlinkTarget + } + return "" +} + +type MakeDirRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MakeDirRequest) Reset() { + *x = MakeDirRequest{} + mi := &file_hostagent_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MakeDirRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MakeDirRequest) ProtoMessage() {} + +func (x *MakeDirRequest) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MakeDirRequest.ProtoReflect.Descriptor instead. +func (*MakeDirRequest) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{34} +} + +func (x *MakeDirRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *MakeDirRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type MakeDirResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entry *FileEntry `protobuf:"bytes,1,opt,name=entry,proto3" json:"entry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MakeDirResponse) Reset() { + *x = MakeDirResponse{} + mi := &file_hostagent_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MakeDirResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MakeDirResponse) ProtoMessage() {} + +func (x *MakeDirResponse) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MakeDirResponse.ProtoReflect.Descriptor instead. +func (*MakeDirResponse) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{35} +} + +func (x *MakeDirResponse) GetEntry() *FileEntry { + if x != nil { + return x.Entry + } + return nil +} + +type RemovePathRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemovePathRequest) Reset() { + *x = RemovePathRequest{} + mi := &file_hostagent_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemovePathRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemovePathRequest) ProtoMessage() {} + +func (x *RemovePathRequest) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemovePathRequest.ProtoReflect.Descriptor instead. +func (*RemovePathRequest) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{36} +} + +func (x *RemovePathRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *RemovePathRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type RemovePathResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemovePathResponse) Reset() { + *x = RemovePathResponse{} + mi := &file_hostagent_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemovePathResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemovePathResponse) ProtoMessage() {} + +func (x *RemovePathResponse) ProtoReflect() protoreflect.Message { + mi := &file_hostagent_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemovePathResponse.ProtoReflect.Descriptor instead. +func (*RemovePathResponse) Descriptor() ([]byte, []int) { + return file_hostagent_proto_rawDescGZIP(), []int{37} +} + type PingSandboxRequest struct { state protoimpl.MessageState `protogen:"open.v1"` SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` @@ -1842,7 +2249,7 @@ type PingSandboxRequest struct { func (x *PingSandboxRequest) Reset() { *x = PingSandboxRequest{} - mi := &file_hostagent_proto_msgTypes[31] + mi := &file_hostagent_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1854,7 +2261,7 @@ func (x *PingSandboxRequest) String() string { func (*PingSandboxRequest) ProtoMessage() {} func (x *PingSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_hostagent_proto_msgTypes[31] + mi := &file_hostagent_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1867,7 +2274,7 @@ func (x *PingSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PingSandboxRequest.ProtoReflect.Descriptor instead. func (*PingSandboxRequest) Descriptor() ([]byte, []int) { - return file_hostagent_proto_rawDescGZIP(), []int{31} + return file_hostagent_proto_rawDescGZIP(), []int{38} } func (x *PingSandboxRequest) GetSandboxId() string { @@ -1885,7 +2292,7 @@ type PingSandboxResponse struct { func (x *PingSandboxResponse) Reset() { *x = PingSandboxResponse{} - mi := &file_hostagent_proto_msgTypes[32] + mi := &file_hostagent_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1897,7 +2304,7 @@ func (x *PingSandboxResponse) String() string { func (*PingSandboxResponse) ProtoMessage() {} func (x *PingSandboxResponse) ProtoReflect() protoreflect.Message { - mi := &file_hostagent_proto_msgTypes[32] + mi := &file_hostagent_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1910,7 +2317,7 @@ func (x *PingSandboxResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PingSandboxResponse.ProtoReflect.Descriptor instead. func (*PingSandboxResponse) Descriptor() ([]byte, []int) { - return file_hostagent_proto_rawDescGZIP(), []int{32} + return file_hostagent_proto_rawDescGZIP(), []int{39} } type TerminateRequest struct { @@ -1921,7 +2328,7 @@ type TerminateRequest struct { func (x *TerminateRequest) Reset() { *x = TerminateRequest{} - mi := &file_hostagent_proto_msgTypes[33] + mi := &file_hostagent_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1933,7 +2340,7 @@ func (x *TerminateRequest) String() string { func (*TerminateRequest) ProtoMessage() {} func (x *TerminateRequest) ProtoReflect() protoreflect.Message { - mi := &file_hostagent_proto_msgTypes[33] + mi := &file_hostagent_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1946,7 +2353,7 @@ func (x *TerminateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TerminateRequest.ProtoReflect.Descriptor instead. func (*TerminateRequest) Descriptor() ([]byte, []int) { - return file_hostagent_proto_rawDescGZIP(), []int{33} + return file_hostagent_proto_rawDescGZIP(), []int{40} } type TerminateResponse struct { @@ -1957,7 +2364,7 @@ type TerminateResponse struct { func (x *TerminateResponse) Reset() { *x = TerminateResponse{} - mi := &file_hostagent_proto_msgTypes[34] + mi := &file_hostagent_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1969,7 +2376,7 @@ func (x *TerminateResponse) String() string { func (*TerminateResponse) ProtoMessage() {} func (x *TerminateResponse) ProtoReflect() protoreflect.Message { - mi := &file_hostagent_proto_msgTypes[34] + mi := &file_hostagent_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1982,7 +2389,7 @@ func (x *TerminateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TerminateResponse.ProtoReflect.Descriptor instead. func (*TerminateResponse) Descriptor() ([]byte, []int) { - return file_hostagent_proto_rawDescGZIP(), []int{34} + return file_hostagent_proto_rawDescGZIP(), []int{41} } type MetricPoint struct { @@ -1997,7 +2404,7 @@ type MetricPoint struct { func (x *MetricPoint) Reset() { *x = MetricPoint{} - mi := &file_hostagent_proto_msgTypes[35] + mi := &file_hostagent_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2009,7 +2416,7 @@ func (x *MetricPoint) String() string { func (*MetricPoint) ProtoMessage() {} func (x *MetricPoint) ProtoReflect() protoreflect.Message { - mi := &file_hostagent_proto_msgTypes[35] + mi := &file_hostagent_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2022,7 +2429,7 @@ func (x *MetricPoint) ProtoReflect() protoreflect.Message { // Deprecated: Use MetricPoint.ProtoReflect.Descriptor instead. func (*MetricPoint) Descriptor() ([]byte, []int) { - return file_hostagent_proto_rawDescGZIP(), []int{35} + return file_hostagent_proto_rawDescGZIP(), []int{42} } func (x *MetricPoint) GetTimestampUnix() int64 { @@ -2064,7 +2471,7 @@ type GetSandboxMetricsRequest struct { func (x *GetSandboxMetricsRequest) Reset() { *x = GetSandboxMetricsRequest{} - mi := &file_hostagent_proto_msgTypes[36] + mi := &file_hostagent_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2076,7 +2483,7 @@ func (x *GetSandboxMetricsRequest) String() string { func (*GetSandboxMetricsRequest) ProtoMessage() {} func (x *GetSandboxMetricsRequest) ProtoReflect() protoreflect.Message { - mi := &file_hostagent_proto_msgTypes[36] + mi := &file_hostagent_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2089,7 +2496,7 @@ func (x *GetSandboxMetricsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxMetricsRequest.ProtoReflect.Descriptor instead. func (*GetSandboxMetricsRequest) Descriptor() ([]byte, []int) { - return file_hostagent_proto_rawDescGZIP(), []int{36} + return file_hostagent_proto_rawDescGZIP(), []int{43} } func (x *GetSandboxMetricsRequest) GetSandboxId() string { @@ -2115,7 +2522,7 @@ type GetSandboxMetricsResponse struct { func (x *GetSandboxMetricsResponse) Reset() { *x = GetSandboxMetricsResponse{} - mi := &file_hostagent_proto_msgTypes[37] + mi := &file_hostagent_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2127,7 +2534,7 @@ func (x *GetSandboxMetricsResponse) String() string { func (*GetSandboxMetricsResponse) ProtoMessage() {} func (x *GetSandboxMetricsResponse) ProtoReflect() protoreflect.Message { - mi := &file_hostagent_proto_msgTypes[37] + mi := &file_hostagent_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2140,7 +2547,7 @@ func (x *GetSandboxMetricsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxMetricsResponse.ProtoReflect.Descriptor instead. func (*GetSandboxMetricsResponse) Descriptor() ([]byte, []int) { - return file_hostagent_proto_rawDescGZIP(), []int{37} + return file_hostagent_proto_rawDescGZIP(), []int{44} } func (x *GetSandboxMetricsResponse) GetPoints() []*MetricPoint { @@ -2159,7 +2566,7 @@ type FlushSandboxMetricsRequest struct { func (x *FlushSandboxMetricsRequest) Reset() { *x = FlushSandboxMetricsRequest{} - mi := &file_hostagent_proto_msgTypes[38] + mi := &file_hostagent_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2171,7 +2578,7 @@ func (x *FlushSandboxMetricsRequest) String() string { func (*FlushSandboxMetricsRequest) ProtoMessage() {} func (x *FlushSandboxMetricsRequest) ProtoReflect() protoreflect.Message { - mi := &file_hostagent_proto_msgTypes[38] + mi := &file_hostagent_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2184,7 +2591,7 @@ func (x *FlushSandboxMetricsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FlushSandboxMetricsRequest.ProtoReflect.Descriptor instead. func (*FlushSandboxMetricsRequest) Descriptor() ([]byte, []int) { - return file_hostagent_proto_rawDescGZIP(), []int{38} + return file_hostagent_proto_rawDescGZIP(), []int{45} } func (x *FlushSandboxMetricsRequest) GetSandboxId() string { @@ -2205,7 +2612,7 @@ type FlushSandboxMetricsResponse struct { func (x *FlushSandboxMetricsResponse) Reset() { *x = FlushSandboxMetricsResponse{} - mi := &file_hostagent_proto_msgTypes[39] + mi := &file_hostagent_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2217,7 +2624,7 @@ func (x *FlushSandboxMetricsResponse) String() string { func (*FlushSandboxMetricsResponse) ProtoMessage() {} func (x *FlushSandboxMetricsResponse) ProtoReflect() protoreflect.Message { - mi := &file_hostagent_proto_msgTypes[39] + mi := &file_hostagent_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2230,7 +2637,7 @@ func (x *FlushSandboxMetricsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FlushSandboxMetricsResponse.ProtoReflect.Descriptor instead. func (*FlushSandboxMetricsResponse) Descriptor() ([]byte, []int) { - return file_hostagent_proto_rawDescGZIP(), []int{39} + return file_hostagent_proto_rawDescGZIP(), []int{46} } func (x *FlushSandboxMetricsResponse) GetPoints_10M() []*MetricPoint { @@ -2269,7 +2676,7 @@ type FlattenRootfsRequest struct { func (x *FlattenRootfsRequest) Reset() { *x = FlattenRootfsRequest{} - mi := &file_hostagent_proto_msgTypes[40] + mi := &file_hostagent_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2281,7 +2688,7 @@ func (x *FlattenRootfsRequest) String() string { func (*FlattenRootfsRequest) ProtoMessage() {} func (x *FlattenRootfsRequest) ProtoReflect() protoreflect.Message { - mi := &file_hostagent_proto_msgTypes[40] + mi := &file_hostagent_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2294,7 +2701,7 @@ func (x *FlattenRootfsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FlattenRootfsRequest.ProtoReflect.Descriptor instead. func (*FlattenRootfsRequest) Descriptor() ([]byte, []int) { - return file_hostagent_proto_rawDescGZIP(), []int{40} + return file_hostagent_proto_rawDescGZIP(), []int{47} } func (x *FlattenRootfsRequest) GetSandboxId() string { @@ -2334,7 +2741,7 @@ type FlattenRootfsResponse struct { func (x *FlattenRootfsResponse) Reset() { *x = FlattenRootfsResponse{} - mi := &file_hostagent_proto_msgTypes[41] + mi := &file_hostagent_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2346,7 +2753,7 @@ func (x *FlattenRootfsResponse) String() string { func (*FlattenRootfsResponse) ProtoMessage() {} func (x *FlattenRootfsResponse) ProtoReflect() protoreflect.Message { - mi := &file_hostagent_proto_msgTypes[41] + mi := &file_hostagent_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2359,7 +2766,7 @@ func (x *FlattenRootfsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FlattenRootfsResponse.ProtoReflect.Descriptor instead. func (*FlattenRootfsResponse) Descriptor() ([]byte, []int) { - return file_hostagent_proto_rawDescGZIP(), []int{41} + return file_hostagent_proto_rawDescGZIP(), []int{48} } func (x *FlattenRootfsResponse) GetSizeBytes() int64 { @@ -2505,7 +2912,39 @@ const file_hostagent_proto_rawDesc = "" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\".\n" + "\x16ReadFileStreamResponse\x12\x14\n" + - "\x05chunk\x18\x01 \x01(\fR\x05chunk\"3\n" + + "\x05chunk\x18\x01 \x01(\fR\x05chunk\"Y\n" + + "\x0eListDirRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x14\n" + + "\x05depth\x18\x03 \x01(\rR\x05depth\"D\n" + + "\x0fListDirResponse\x121\n" + + "\aentries\x18\x01 \x03(\v2\x17.hostagent.v1.FileEntryR\aentries\"\x9d\x02\n" + + "\tFileEntry\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\x12\x12\n" + + "\x04mode\x18\x05 \x01(\rR\x04mode\x12 \n" + + "\vpermissions\x18\x06 \x01(\tR\vpermissions\x12\x14\n" + + "\x05owner\x18\a \x01(\tR\x05owner\x12\x14\n" + + "\x05group\x18\b \x01(\tR\x05group\x12\x1f\n" + + "\vmodified_at\x18\t \x01(\x03R\n" + + "modifiedAt\x12*\n" + + "\x0esymlink_target\x18\n" + + " \x01(\tH\x00R\rsymlinkTarget\x88\x01\x01B\x11\n" + + "\x0f_symlink_target\"C\n" + + "\x0eMakeDirRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\"@\n" + + "\x0fMakeDirResponse\x12-\n" + + "\x05entry\x18\x01 \x01(\v2\x17.hostagent.v1.FileEntryR\x05entry\"F\n" + + "\x11RemovePathRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\"\x14\n" + + "\x12RemovePathResponse\"3\n" + "\x12PingSandboxRequest\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x15\n" + @@ -2542,7 +2981,7 @@ const file_hostagent_proto_rawDesc = "" + "templateId\"6\n" + "\x15FlattenRootfsResponse\x12\x1d\n" + "\n" + - "size_bytes\x18\x01 \x01(\x03R\tsizeBytes2\xc8\f\n" + + "size_bytes\x18\x01 \x01(\x03R\tsizeBytes2\xa9\x0e\n" + "\x10HostAgentService\x12X\n" + "\rCreateSandbox\x12\".hostagent.v1.CreateSandboxRequest\x1a#.hostagent.v1.CreateSandboxResponse\x12[\n" + "\x0eDestroySandbox\x12#.hostagent.v1.DestroySandboxRequest\x1a$.hostagent.v1.DestroySandboxResponse\x12U\n" + @@ -2551,7 +2990,11 @@ const file_hostagent_proto_rawDesc = "" + "\x04Exec\x12\x19.hostagent.v1.ExecRequest\x1a\x1a.hostagent.v1.ExecResponse\x12X\n" + "\rListSandboxes\x12\".hostagent.v1.ListSandboxesRequest\x1a#.hostagent.v1.ListSandboxesResponse\x12L\n" + "\tWriteFile\x12\x1e.hostagent.v1.WriteFileRequest\x1a\x1f.hostagent.v1.WriteFileResponse\x12I\n" + - "\bReadFile\x12\x1d.hostagent.v1.ReadFileRequest\x1a\x1e.hostagent.v1.ReadFileResponse\x12[\n" + + "\bReadFile\x12\x1d.hostagent.v1.ReadFileRequest\x1a\x1e.hostagent.v1.ReadFileResponse\x12F\n" + + "\aListDir\x12\x1c.hostagent.v1.ListDirRequest\x1a\x1d.hostagent.v1.ListDirResponse\x12F\n" + + "\aMakeDir\x12\x1c.hostagent.v1.MakeDirRequest\x1a\x1d.hostagent.v1.MakeDirResponse\x12O\n" + + "\n" + + "RemovePath\x12\x1f.hostagent.v1.RemovePathRequest\x1a .hostagent.v1.RemovePathResponse\x12[\n" + "\x0eCreateSnapshot\x12#.hostagent.v1.CreateSnapshotRequest\x1a$.hostagent.v1.CreateSnapshotResponse\x12[\n" + "\x0eDeleteSnapshot\x12#.hostagent.v1.DeleteSnapshotRequest\x1a$.hostagent.v1.DeleteSnapshotResponse\x12Q\n" + "\n" + @@ -2577,7 +3020,7 @@ func file_hostagent_proto_rawDescGZIP() []byte { return file_hostagent_proto_rawDescData } -var file_hostagent_proto_msgTypes = make([]protoimpl.MessageInfo, 42) +var file_hostagent_proto_msgTypes = make([]protoimpl.MessageInfo, 49) var file_hostagent_proto_goTypes = []any{ (*CreateSandboxRequest)(nil), // 0: hostagent.v1.CreateSandboxRequest (*CreateSandboxResponse)(nil), // 1: hostagent.v1.CreateSandboxResponse @@ -2610,17 +3053,24 @@ var file_hostagent_proto_goTypes = []any{ (*WriteFileStreamResponse)(nil), // 28: hostagent.v1.WriteFileStreamResponse (*ReadFileStreamRequest)(nil), // 29: hostagent.v1.ReadFileStreamRequest (*ReadFileStreamResponse)(nil), // 30: hostagent.v1.ReadFileStreamResponse - (*PingSandboxRequest)(nil), // 31: hostagent.v1.PingSandboxRequest - (*PingSandboxResponse)(nil), // 32: hostagent.v1.PingSandboxResponse - (*TerminateRequest)(nil), // 33: hostagent.v1.TerminateRequest - (*TerminateResponse)(nil), // 34: hostagent.v1.TerminateResponse - (*MetricPoint)(nil), // 35: hostagent.v1.MetricPoint - (*GetSandboxMetricsRequest)(nil), // 36: hostagent.v1.GetSandboxMetricsRequest - (*GetSandboxMetricsResponse)(nil), // 37: hostagent.v1.GetSandboxMetricsResponse - (*FlushSandboxMetricsRequest)(nil), // 38: hostagent.v1.FlushSandboxMetricsRequest - (*FlushSandboxMetricsResponse)(nil), // 39: hostagent.v1.FlushSandboxMetricsResponse - (*FlattenRootfsRequest)(nil), // 40: hostagent.v1.FlattenRootfsRequest - (*FlattenRootfsResponse)(nil), // 41: hostagent.v1.FlattenRootfsResponse + (*ListDirRequest)(nil), // 31: hostagent.v1.ListDirRequest + (*ListDirResponse)(nil), // 32: hostagent.v1.ListDirResponse + (*FileEntry)(nil), // 33: hostagent.v1.FileEntry + (*MakeDirRequest)(nil), // 34: hostagent.v1.MakeDirRequest + (*MakeDirResponse)(nil), // 35: hostagent.v1.MakeDirResponse + (*RemovePathRequest)(nil), // 36: hostagent.v1.RemovePathRequest + (*RemovePathResponse)(nil), // 37: hostagent.v1.RemovePathResponse + (*PingSandboxRequest)(nil), // 38: hostagent.v1.PingSandboxRequest + (*PingSandboxResponse)(nil), // 39: hostagent.v1.PingSandboxResponse + (*TerminateRequest)(nil), // 40: hostagent.v1.TerminateRequest + (*TerminateResponse)(nil), // 41: hostagent.v1.TerminateResponse + (*MetricPoint)(nil), // 42: hostagent.v1.MetricPoint + (*GetSandboxMetricsRequest)(nil), // 43: hostagent.v1.GetSandboxMetricsRequest + (*GetSandboxMetricsResponse)(nil), // 44: hostagent.v1.GetSandboxMetricsResponse + (*FlushSandboxMetricsRequest)(nil), // 45: hostagent.v1.FlushSandboxMetricsRequest + (*FlushSandboxMetricsResponse)(nil), // 46: hostagent.v1.FlushSandboxMetricsResponse + (*FlattenRootfsRequest)(nil), // 47: hostagent.v1.FlattenRootfsRequest + (*FlattenRootfsResponse)(nil), // 48: hostagent.v1.FlattenRootfsResponse } var file_hostagent_proto_depIdxs = []int32{ 16, // 0: hostagent.v1.ListSandboxesResponse.sandboxes:type_name -> hostagent.v1.SandboxInfo @@ -2628,51 +3078,59 @@ var file_hostagent_proto_depIdxs = []int32{ 24, // 2: hostagent.v1.ExecStreamResponse.data:type_name -> hostagent.v1.ExecStreamData 25, // 3: hostagent.v1.ExecStreamResponse.end:type_name -> hostagent.v1.ExecStreamEnd 27, // 4: hostagent.v1.WriteFileStreamRequest.meta:type_name -> hostagent.v1.WriteFileStreamMeta - 35, // 5: hostagent.v1.GetSandboxMetricsResponse.points:type_name -> hostagent.v1.MetricPoint - 35, // 6: hostagent.v1.FlushSandboxMetricsResponse.points_10m:type_name -> hostagent.v1.MetricPoint - 35, // 7: hostagent.v1.FlushSandboxMetricsResponse.points_2h:type_name -> hostagent.v1.MetricPoint - 35, // 8: hostagent.v1.FlushSandboxMetricsResponse.points_24h:type_name -> hostagent.v1.MetricPoint - 0, // 9: hostagent.v1.HostAgentService.CreateSandbox:input_type -> hostagent.v1.CreateSandboxRequest - 2, // 10: hostagent.v1.HostAgentService.DestroySandbox:input_type -> hostagent.v1.DestroySandboxRequest - 4, // 11: hostagent.v1.HostAgentService.PauseSandbox:input_type -> hostagent.v1.PauseSandboxRequest - 6, // 12: hostagent.v1.HostAgentService.ResumeSandbox:input_type -> hostagent.v1.ResumeSandboxRequest - 12, // 13: hostagent.v1.HostAgentService.Exec:input_type -> hostagent.v1.ExecRequest - 14, // 14: hostagent.v1.HostAgentService.ListSandboxes:input_type -> hostagent.v1.ListSandboxesRequest - 17, // 15: hostagent.v1.HostAgentService.WriteFile:input_type -> hostagent.v1.WriteFileRequest - 19, // 16: hostagent.v1.HostAgentService.ReadFile:input_type -> hostagent.v1.ReadFileRequest - 8, // 17: hostagent.v1.HostAgentService.CreateSnapshot:input_type -> hostagent.v1.CreateSnapshotRequest - 10, // 18: hostagent.v1.HostAgentService.DeleteSnapshot:input_type -> hostagent.v1.DeleteSnapshotRequest - 21, // 19: hostagent.v1.HostAgentService.ExecStream:input_type -> hostagent.v1.ExecStreamRequest - 26, // 20: hostagent.v1.HostAgentService.WriteFileStream:input_type -> hostagent.v1.WriteFileStreamRequest - 29, // 21: hostagent.v1.HostAgentService.ReadFileStream:input_type -> hostagent.v1.ReadFileStreamRequest - 31, // 22: hostagent.v1.HostAgentService.PingSandbox:input_type -> hostagent.v1.PingSandboxRequest - 33, // 23: hostagent.v1.HostAgentService.Terminate:input_type -> hostagent.v1.TerminateRequest - 36, // 24: hostagent.v1.HostAgentService.GetSandboxMetrics:input_type -> hostagent.v1.GetSandboxMetricsRequest - 38, // 25: hostagent.v1.HostAgentService.FlushSandboxMetrics:input_type -> hostagent.v1.FlushSandboxMetricsRequest - 40, // 26: hostagent.v1.HostAgentService.FlattenRootfs:input_type -> hostagent.v1.FlattenRootfsRequest - 1, // 27: hostagent.v1.HostAgentService.CreateSandbox:output_type -> hostagent.v1.CreateSandboxResponse - 3, // 28: hostagent.v1.HostAgentService.DestroySandbox:output_type -> hostagent.v1.DestroySandboxResponse - 5, // 29: hostagent.v1.HostAgentService.PauseSandbox:output_type -> hostagent.v1.PauseSandboxResponse - 7, // 30: hostagent.v1.HostAgentService.ResumeSandbox:output_type -> hostagent.v1.ResumeSandboxResponse - 13, // 31: hostagent.v1.HostAgentService.Exec:output_type -> hostagent.v1.ExecResponse - 15, // 32: hostagent.v1.HostAgentService.ListSandboxes:output_type -> hostagent.v1.ListSandboxesResponse - 18, // 33: hostagent.v1.HostAgentService.WriteFile:output_type -> hostagent.v1.WriteFileResponse - 20, // 34: hostagent.v1.HostAgentService.ReadFile:output_type -> hostagent.v1.ReadFileResponse - 9, // 35: hostagent.v1.HostAgentService.CreateSnapshot:output_type -> hostagent.v1.CreateSnapshotResponse - 11, // 36: hostagent.v1.HostAgentService.DeleteSnapshot:output_type -> hostagent.v1.DeleteSnapshotResponse - 22, // 37: hostagent.v1.HostAgentService.ExecStream:output_type -> hostagent.v1.ExecStreamResponse - 28, // 38: hostagent.v1.HostAgentService.WriteFileStream:output_type -> hostagent.v1.WriteFileStreamResponse - 30, // 39: hostagent.v1.HostAgentService.ReadFileStream:output_type -> hostagent.v1.ReadFileStreamResponse - 32, // 40: hostagent.v1.HostAgentService.PingSandbox:output_type -> hostagent.v1.PingSandboxResponse - 34, // 41: hostagent.v1.HostAgentService.Terminate:output_type -> hostagent.v1.TerminateResponse - 37, // 42: hostagent.v1.HostAgentService.GetSandboxMetrics:output_type -> hostagent.v1.GetSandboxMetricsResponse - 39, // 43: hostagent.v1.HostAgentService.FlushSandboxMetrics:output_type -> hostagent.v1.FlushSandboxMetricsResponse - 41, // 44: hostagent.v1.HostAgentService.FlattenRootfs:output_type -> hostagent.v1.FlattenRootfsResponse - 27, // [27:45] is the sub-list for method output_type - 9, // [9:27] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 33, // 5: hostagent.v1.ListDirResponse.entries:type_name -> hostagent.v1.FileEntry + 33, // 6: hostagent.v1.MakeDirResponse.entry:type_name -> hostagent.v1.FileEntry + 42, // 7: hostagent.v1.GetSandboxMetricsResponse.points:type_name -> hostagent.v1.MetricPoint + 42, // 8: hostagent.v1.FlushSandboxMetricsResponse.points_10m:type_name -> hostagent.v1.MetricPoint + 42, // 9: hostagent.v1.FlushSandboxMetricsResponse.points_2h:type_name -> hostagent.v1.MetricPoint + 42, // 10: hostagent.v1.FlushSandboxMetricsResponse.points_24h:type_name -> hostagent.v1.MetricPoint + 0, // 11: hostagent.v1.HostAgentService.CreateSandbox:input_type -> hostagent.v1.CreateSandboxRequest + 2, // 12: hostagent.v1.HostAgentService.DestroySandbox:input_type -> hostagent.v1.DestroySandboxRequest + 4, // 13: hostagent.v1.HostAgentService.PauseSandbox:input_type -> hostagent.v1.PauseSandboxRequest + 6, // 14: hostagent.v1.HostAgentService.ResumeSandbox:input_type -> hostagent.v1.ResumeSandboxRequest + 12, // 15: hostagent.v1.HostAgentService.Exec:input_type -> hostagent.v1.ExecRequest + 14, // 16: hostagent.v1.HostAgentService.ListSandboxes:input_type -> hostagent.v1.ListSandboxesRequest + 17, // 17: hostagent.v1.HostAgentService.WriteFile:input_type -> hostagent.v1.WriteFileRequest + 19, // 18: hostagent.v1.HostAgentService.ReadFile:input_type -> hostagent.v1.ReadFileRequest + 31, // 19: hostagent.v1.HostAgentService.ListDir:input_type -> hostagent.v1.ListDirRequest + 34, // 20: hostagent.v1.HostAgentService.MakeDir:input_type -> hostagent.v1.MakeDirRequest + 36, // 21: hostagent.v1.HostAgentService.RemovePath:input_type -> hostagent.v1.RemovePathRequest + 8, // 22: hostagent.v1.HostAgentService.CreateSnapshot:input_type -> hostagent.v1.CreateSnapshotRequest + 10, // 23: hostagent.v1.HostAgentService.DeleteSnapshot:input_type -> hostagent.v1.DeleteSnapshotRequest + 21, // 24: hostagent.v1.HostAgentService.ExecStream:input_type -> hostagent.v1.ExecStreamRequest + 26, // 25: hostagent.v1.HostAgentService.WriteFileStream:input_type -> hostagent.v1.WriteFileStreamRequest + 29, // 26: hostagent.v1.HostAgentService.ReadFileStream:input_type -> hostagent.v1.ReadFileStreamRequest + 38, // 27: hostagent.v1.HostAgentService.PingSandbox:input_type -> hostagent.v1.PingSandboxRequest + 40, // 28: hostagent.v1.HostAgentService.Terminate:input_type -> hostagent.v1.TerminateRequest + 43, // 29: hostagent.v1.HostAgentService.GetSandboxMetrics:input_type -> hostagent.v1.GetSandboxMetricsRequest + 45, // 30: hostagent.v1.HostAgentService.FlushSandboxMetrics:input_type -> hostagent.v1.FlushSandboxMetricsRequest + 47, // 31: hostagent.v1.HostAgentService.FlattenRootfs:input_type -> hostagent.v1.FlattenRootfsRequest + 1, // 32: hostagent.v1.HostAgentService.CreateSandbox:output_type -> hostagent.v1.CreateSandboxResponse + 3, // 33: hostagent.v1.HostAgentService.DestroySandbox:output_type -> hostagent.v1.DestroySandboxResponse + 5, // 34: hostagent.v1.HostAgentService.PauseSandbox:output_type -> hostagent.v1.PauseSandboxResponse + 7, // 35: hostagent.v1.HostAgentService.ResumeSandbox:output_type -> hostagent.v1.ResumeSandboxResponse + 13, // 36: hostagent.v1.HostAgentService.Exec:output_type -> hostagent.v1.ExecResponse + 15, // 37: hostagent.v1.HostAgentService.ListSandboxes:output_type -> hostagent.v1.ListSandboxesResponse + 18, // 38: hostagent.v1.HostAgentService.WriteFile:output_type -> hostagent.v1.WriteFileResponse + 20, // 39: hostagent.v1.HostAgentService.ReadFile:output_type -> hostagent.v1.ReadFileResponse + 32, // 40: hostagent.v1.HostAgentService.ListDir:output_type -> hostagent.v1.ListDirResponse + 35, // 41: hostagent.v1.HostAgentService.MakeDir:output_type -> hostagent.v1.MakeDirResponse + 37, // 42: hostagent.v1.HostAgentService.RemovePath:output_type -> hostagent.v1.RemovePathResponse + 9, // 43: hostagent.v1.HostAgentService.CreateSnapshot:output_type -> hostagent.v1.CreateSnapshotResponse + 11, // 44: hostagent.v1.HostAgentService.DeleteSnapshot:output_type -> hostagent.v1.DeleteSnapshotResponse + 22, // 45: hostagent.v1.HostAgentService.ExecStream:output_type -> hostagent.v1.ExecStreamResponse + 28, // 46: hostagent.v1.HostAgentService.WriteFileStream:output_type -> hostagent.v1.WriteFileStreamResponse + 30, // 47: hostagent.v1.HostAgentService.ReadFileStream:output_type -> hostagent.v1.ReadFileStreamResponse + 39, // 48: hostagent.v1.HostAgentService.PingSandbox:output_type -> hostagent.v1.PingSandboxResponse + 41, // 49: hostagent.v1.HostAgentService.Terminate:output_type -> hostagent.v1.TerminateResponse + 44, // 50: hostagent.v1.HostAgentService.GetSandboxMetrics:output_type -> hostagent.v1.GetSandboxMetricsResponse + 46, // 51: hostagent.v1.HostAgentService.FlushSandboxMetrics:output_type -> hostagent.v1.FlushSandboxMetricsResponse + 48, // 52: hostagent.v1.HostAgentService.FlattenRootfs:output_type -> hostagent.v1.FlattenRootfsResponse + 32, // [32:53] is the sub-list for method output_type + 11, // [11:32] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_hostagent_proto_init() } @@ -2693,13 +3151,14 @@ func file_hostagent_proto_init() { (*WriteFileStreamRequest_Meta)(nil), (*WriteFileStreamRequest_Chunk)(nil), } + file_hostagent_proto_msgTypes[33].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_hostagent_proto_rawDesc), len(file_hostagent_proto_rawDesc)), NumEnums: 0, - NumMessages: 42, + NumMessages: 49, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/hostagent/gen/hostagentv1connect/hostagent.connect.go b/proto/hostagent/gen/hostagentv1connect/hostagent.connect.go index 924f4b3..aff538a 100644 --- a/proto/hostagent/gen/hostagentv1connect/hostagent.connect.go +++ b/proto/hostagent/gen/hostagentv1connect/hostagent.connect.go @@ -56,6 +56,15 @@ const ( // HostAgentServiceReadFileProcedure is the fully-qualified name of the HostAgentService's ReadFile // RPC. HostAgentServiceReadFileProcedure = "/hostagent.v1.HostAgentService/ReadFile" + // HostAgentServiceListDirProcedure is the fully-qualified name of the HostAgentService's ListDir + // RPC. + HostAgentServiceListDirProcedure = "/hostagent.v1.HostAgentService/ListDir" + // HostAgentServiceMakeDirProcedure is the fully-qualified name of the HostAgentService's MakeDir + // RPC. + HostAgentServiceMakeDirProcedure = "/hostagent.v1.HostAgentService/MakeDir" + // HostAgentServiceRemovePathProcedure is the fully-qualified name of the HostAgentService's + // RemovePath RPC. + HostAgentServiceRemovePathProcedure = "/hostagent.v1.HostAgentService/RemovePath" // HostAgentServiceCreateSnapshotProcedure is the fully-qualified name of the HostAgentService's // CreateSnapshot RPC. HostAgentServiceCreateSnapshotProcedure = "/hostagent.v1.HostAgentService/CreateSnapshot" @@ -106,6 +115,12 @@ type HostAgentServiceClient interface { WriteFile(context.Context, *connect.Request[gen.WriteFileRequest]) (*connect.Response[gen.WriteFileResponse], error) // ReadFile reads a file from inside a sandbox. ReadFile(context.Context, *connect.Request[gen.ReadFileRequest]) (*connect.Response[gen.ReadFileResponse], error) + // ListDir lists directory contents inside a sandbox. + ListDir(context.Context, *connect.Request[gen.ListDirRequest]) (*connect.Response[gen.ListDirResponse], error) + // MakeDir creates a directory inside a sandbox. + MakeDir(context.Context, *connect.Request[gen.MakeDirRequest]) (*connect.Response[gen.MakeDirResponse], error) + // RemovePath removes a file or directory inside a sandbox. + RemovePath(context.Context, *connect.Request[gen.RemovePathRequest]) (*connect.Response[gen.RemovePathResponse], error) // CreateSnapshot pauses a sandbox, takes a snapshot, stores it as a reusable // template, and destroys the sandbox. CreateSnapshot(context.Context, *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error) @@ -195,6 +210,24 @@ func NewHostAgentServiceClient(httpClient connect.HTTPClient, baseURL string, op connect.WithSchema(hostAgentServiceMethods.ByName("ReadFile")), connect.WithClientOptions(opts...), ), + listDir: connect.NewClient[gen.ListDirRequest, gen.ListDirResponse]( + httpClient, + baseURL+HostAgentServiceListDirProcedure, + connect.WithSchema(hostAgentServiceMethods.ByName("ListDir")), + connect.WithClientOptions(opts...), + ), + makeDir: connect.NewClient[gen.MakeDirRequest, gen.MakeDirResponse]( + httpClient, + baseURL+HostAgentServiceMakeDirProcedure, + connect.WithSchema(hostAgentServiceMethods.ByName("MakeDir")), + connect.WithClientOptions(opts...), + ), + removePath: connect.NewClient[gen.RemovePathRequest, gen.RemovePathResponse]( + httpClient, + baseURL+HostAgentServiceRemovePathProcedure, + connect.WithSchema(hostAgentServiceMethods.ByName("RemovePath")), + connect.WithClientOptions(opts...), + ), createSnapshot: connect.NewClient[gen.CreateSnapshotRequest, gen.CreateSnapshotResponse]( httpClient, baseURL+HostAgentServiceCreateSnapshotProcedure, @@ -268,6 +301,9 @@ type hostAgentServiceClient struct { listSandboxes *connect.Client[gen.ListSandboxesRequest, gen.ListSandboxesResponse] writeFile *connect.Client[gen.WriteFileRequest, gen.WriteFileResponse] readFile *connect.Client[gen.ReadFileRequest, gen.ReadFileResponse] + listDir *connect.Client[gen.ListDirRequest, gen.ListDirResponse] + makeDir *connect.Client[gen.MakeDirRequest, gen.MakeDirResponse] + removePath *connect.Client[gen.RemovePathRequest, gen.RemovePathResponse] createSnapshot *connect.Client[gen.CreateSnapshotRequest, gen.CreateSnapshotResponse] deleteSnapshot *connect.Client[gen.DeleteSnapshotRequest, gen.DeleteSnapshotResponse] execStream *connect.Client[gen.ExecStreamRequest, gen.ExecStreamResponse] @@ -320,6 +356,21 @@ func (c *hostAgentServiceClient) ReadFile(ctx context.Context, req *connect.Requ return c.readFile.CallUnary(ctx, req) } +// ListDir calls hostagent.v1.HostAgentService.ListDir. +func (c *hostAgentServiceClient) ListDir(ctx context.Context, req *connect.Request[gen.ListDirRequest]) (*connect.Response[gen.ListDirResponse], error) { + return c.listDir.CallUnary(ctx, req) +} + +// MakeDir calls hostagent.v1.HostAgentService.MakeDir. +func (c *hostAgentServiceClient) MakeDir(ctx context.Context, req *connect.Request[gen.MakeDirRequest]) (*connect.Response[gen.MakeDirResponse], error) { + return c.makeDir.CallUnary(ctx, req) +} + +// RemovePath calls hostagent.v1.HostAgentService.RemovePath. +func (c *hostAgentServiceClient) RemovePath(ctx context.Context, req *connect.Request[gen.RemovePathRequest]) (*connect.Response[gen.RemovePathResponse], error) { + return c.removePath.CallUnary(ctx, req) +} + // CreateSnapshot calls hostagent.v1.HostAgentService.CreateSnapshot. func (c *hostAgentServiceClient) CreateSnapshot(ctx context.Context, req *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error) { return c.createSnapshot.CallUnary(ctx, req) @@ -388,6 +439,12 @@ type HostAgentServiceHandler interface { WriteFile(context.Context, *connect.Request[gen.WriteFileRequest]) (*connect.Response[gen.WriteFileResponse], error) // ReadFile reads a file from inside a sandbox. ReadFile(context.Context, *connect.Request[gen.ReadFileRequest]) (*connect.Response[gen.ReadFileResponse], error) + // ListDir lists directory contents inside a sandbox. + ListDir(context.Context, *connect.Request[gen.ListDirRequest]) (*connect.Response[gen.ListDirResponse], error) + // MakeDir creates a directory inside a sandbox. + MakeDir(context.Context, *connect.Request[gen.MakeDirRequest]) (*connect.Response[gen.MakeDirResponse], error) + // RemovePath removes a file or directory inside a sandbox. + RemovePath(context.Context, *connect.Request[gen.RemovePathRequest]) (*connect.Response[gen.RemovePathResponse], error) // CreateSnapshot pauses a sandbox, takes a snapshot, stores it as a reusable // template, and destroys the sandbox. CreateSnapshot(context.Context, *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error) @@ -473,6 +530,24 @@ func NewHostAgentServiceHandler(svc HostAgentServiceHandler, opts ...connect.Han connect.WithSchema(hostAgentServiceMethods.ByName("ReadFile")), connect.WithHandlerOptions(opts...), ) + hostAgentServiceListDirHandler := connect.NewUnaryHandler( + HostAgentServiceListDirProcedure, + svc.ListDir, + connect.WithSchema(hostAgentServiceMethods.ByName("ListDir")), + connect.WithHandlerOptions(opts...), + ) + hostAgentServiceMakeDirHandler := connect.NewUnaryHandler( + HostAgentServiceMakeDirProcedure, + svc.MakeDir, + connect.WithSchema(hostAgentServiceMethods.ByName("MakeDir")), + connect.WithHandlerOptions(opts...), + ) + hostAgentServiceRemovePathHandler := connect.NewUnaryHandler( + HostAgentServiceRemovePathProcedure, + svc.RemovePath, + connect.WithSchema(hostAgentServiceMethods.ByName("RemovePath")), + connect.WithHandlerOptions(opts...), + ) hostAgentServiceCreateSnapshotHandler := connect.NewUnaryHandler( HostAgentServiceCreateSnapshotProcedure, svc.CreateSnapshot, @@ -551,6 +626,12 @@ func NewHostAgentServiceHandler(svc HostAgentServiceHandler, opts ...connect.Han hostAgentServiceWriteFileHandler.ServeHTTP(w, r) case HostAgentServiceReadFileProcedure: hostAgentServiceReadFileHandler.ServeHTTP(w, r) + case HostAgentServiceListDirProcedure: + hostAgentServiceListDirHandler.ServeHTTP(w, r) + case HostAgentServiceMakeDirProcedure: + hostAgentServiceMakeDirHandler.ServeHTTP(w, r) + case HostAgentServiceRemovePathProcedure: + hostAgentServiceRemovePathHandler.ServeHTTP(w, r) case HostAgentServiceCreateSnapshotProcedure: hostAgentServiceCreateSnapshotHandler.ServeHTTP(w, r) case HostAgentServiceDeleteSnapshotProcedure: @@ -612,6 +693,18 @@ func (UnimplementedHostAgentServiceHandler) ReadFile(context.Context, *connect.R return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.ReadFile is not implemented")) } +func (UnimplementedHostAgentServiceHandler) ListDir(context.Context, *connect.Request[gen.ListDirRequest]) (*connect.Response[gen.ListDirResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.ListDir is not implemented")) +} + +func (UnimplementedHostAgentServiceHandler) MakeDir(context.Context, *connect.Request[gen.MakeDirRequest]) (*connect.Response[gen.MakeDirResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.MakeDir is not implemented")) +} + +func (UnimplementedHostAgentServiceHandler) RemovePath(context.Context, *connect.Request[gen.RemovePathRequest]) (*connect.Response[gen.RemovePathResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.RemovePath is not implemented")) +} + func (UnimplementedHostAgentServiceHandler) CreateSnapshot(context.Context, *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.CreateSnapshot is not implemented")) } diff --git a/proto/hostagent/hostagent.proto b/proto/hostagent/hostagent.proto index 817d535..c5ce615 100644 --- a/proto/hostagent/hostagent.proto +++ b/proto/hostagent/hostagent.proto @@ -29,6 +29,15 @@ service HostAgentService { // ReadFile reads a file from inside a sandbox. rpc ReadFile(ReadFileRequest) returns (ReadFileResponse); + // ListDir lists directory contents inside a sandbox. + rpc ListDir(ListDirRequest) returns (ListDirResponse); + + // MakeDir creates a directory inside a sandbox. + rpc MakeDir(MakeDirRequest) returns (MakeDirResponse); + + // RemovePath removes a file or directory inside a sandbox. + rpc RemovePath(RemovePathRequest) returns (RemovePathResponse); + // CreateSnapshot pauses a sandbox, takes a snapshot, stores it as a reusable // template, and destroys the sandbox. rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse); @@ -269,6 +278,50 @@ message ReadFileStreamResponse { bytes chunk = 1; } +// ── Filesystem Operations ────────────────────────────────────────── + +message ListDirRequest { + string sandbox_id = 1; + string path = 2; + uint32 depth = 3; +} + +message ListDirResponse { + repeated FileEntry entries = 1; +} + +message FileEntry { + string name = 1; + string path = 2; + // "file", "directory", or "symlink". + string type = 3; + int64 size = 4; + uint32 mode = 5; + // Human-readable permissions string, e.g. "-rwxr-xr-x". + string permissions = 6; + string owner = 7; + string group = 8; + // Last modification time as Unix timestamp (seconds). + int64 modified_at = 9; + optional string symlink_target = 10; +} + +message MakeDirRequest { + string sandbox_id = 1; + string path = 2; +} + +message MakeDirResponse { + FileEntry entry = 1; +} + +message RemovePathRequest { + string sandbox_id = 1; + string path = 2; +} + +message RemovePathResponse {} + // ── Ping ──────────────────────────────────────────────────────────── message PingSandboxRequest { From 82531b735cf7e0d38671f71dc1525e90fbab4c27 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Fri, 10 Apr 2026 18:43:11 +0600 Subject: [PATCH 2/5] Add Files tab to capsule detail page with file browser and preview Implements a split-panel file browser: directory tree on the left with path input and breadcrumb navigation, file preview on the right with line numbers. Binary/large files (>10MB) show a download prompt instead. Also adds CopyButton component across capsule, snapshot, and template pages, and fixes pre-existing type errors in StatsPanel and admin templates page. --- frontend/src/lib/api/files.ts | 124 ++++ frontend/src/lib/components/CopyButton.svelte | 112 ++++ frontend/src/lib/components/FilesTab.svelte | 546 ++++++++++++++++++ frontend/src/lib/components/StatsPanel.svelte | 7 +- .../src/routes/admin/templates/+page.svelte | 8 +- .../routes/dashboard/capsules/+layout.svelte | 8 +- .../routes/dashboard/capsules/+page.svelte | 2 + .../dashboard/capsules/[id]/+page.svelte | 80 ++- .../routes/dashboard/snapshots/+page.svelte | 6 +- 9 files changed, 861 insertions(+), 32 deletions(-) create mode 100644 frontend/src/lib/api/files.ts create mode 100644 frontend/src/lib/components/CopyButton.svelte create mode 100644 frontend/src/lib/components/FilesTab.svelte diff --git a/frontend/src/lib/api/files.ts b/frontend/src/lib/api/files.ts new file mode 100644 index 0000000..c0f1852 --- /dev/null +++ b/frontend/src/lib/api/files.ts @@ -0,0 +1,124 @@ +import { auth } from '$lib/auth.svelte'; +import { type ApiResult } from '$lib/api/client'; + +export type FileEntry = { + name: string; + path: string; + type: 'file' | 'directory' | 'symlink'; + size: number; + mode: number; + permissions: string; + owner: string; + group: string; + modified_at: number; + symlink_target?: string | null; +}; + +export type ListDirResponse = { + entries: FileEntry[]; +}; + +const MAX_READABLE_SIZE = 10 * 1024 * 1024; // 10 MB + +/** + * Whether a file can be previewed as text in the browser. + * Binary/unreadable extensions and files > 10 MB should be downloaded instead. + */ +const BINARY_EXTENSIONS = new Set([ + '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.avif', '.svg', + '.mp3', '.mp4', '.wav', '.ogg', '.flac', '.avi', '.mkv', '.mov', '.webm', + '.zip', '.tar', '.gz', '.bz2', '.xz', '.7z', '.rar', '.zst', + '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', + '.exe', '.dll', '.so', '.dylib', '.bin', '.o', '.a', '.class', '.pyc', + '.woff', '.woff2', '.ttf', '.otf', '.eot', + '.db', '.sqlite', '.sqlite3', + '.iso', '.img', '.dmg', +]); + +export function isBinaryFile(name: string): boolean { + const dot = name.lastIndexOf('.'); + if (dot === -1) return false; + return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase()); +} + +export function isFileTooLarge(size: number): boolean { + return size > MAX_READABLE_SIZE; +} + +export function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const val = bytes / Math.pow(1024, i); + return `${val < 10 ? val.toFixed(1) : Math.round(val)} ${units[i]}`; +} + +export async function listDir(sandboxId: string, path: string, depth = 1): Promise> { + try { + const headers: Record = { 'Content-Type': 'application/json' }; + if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`; + + const res = await fetch(`/api/v1/sandboxes/${sandboxId}/files/list`, { + method: 'POST', + headers, + body: JSON.stringify({ path, depth }), + }); + + const data = await res.json(); + if (!res.ok) return { ok: false, error: data?.error?.message ?? 'Failed to list directory' }; + return { ok: true, data: data as ListDirResponse }; + } catch { + return { ok: false, error: 'Unable to connect to the server' }; + } +} + +export async function readFile(sandboxId: string, path: string): Promise> { + try { + const headers: Record = { 'Content-Type': 'application/json' }; + if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`; + + const res = await fetch(`/api/v1/sandboxes/${sandboxId}/files/read`, { + method: 'POST', + headers, + body: JSON.stringify({ path }), + }); + + if (!res.ok) { + try { + const data = await res.json(); + return { ok: false, error: data?.error?.message ?? 'Failed to read file' }; + } catch { + return { ok: false, error: `HTTP ${res.status}` }; + } + } + + const blob = await res.blob(); + const text = await blob.text(); + return { ok: true, data: text }; + } catch { + return { ok: false, error: 'Unable to connect to the server' }; + } +} + +export async function downloadFile(sandboxId: string, path: string, filename: string): Promise { + const headers: Record = { 'Content-Type': 'application/json' }; + if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`; + + const res = await fetch(`/api/v1/sandboxes/${sandboxId}/files/read`, { + method: 'POST', + headers, + body: JSON.stringify({ path }), + }); + + if (!res.ok) throw new Error('Download failed'); + + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} diff --git a/frontend/src/lib/components/CopyButton.svelte b/frontend/src/lib/components/CopyButton.svelte new file mode 100644 index 0000000..97ca2be --- /dev/null +++ b/frontend/src/lib/components/CopyButton.svelte @@ -0,0 +1,112 @@ + + + + + diff --git a/frontend/src/lib/components/FilesTab.svelte b/frontend/src/lib/components/FilesTab.svelte new file mode 100644 index 0000000..ea5bcc9 --- /dev/null +++ b/frontend/src/lib/components/FilesTab.svelte @@ -0,0 +1,546 @@ + + + + +{#if !isRunning} +
+ + + File browser is only available for running capsules. + +
+{:else} +
+ + +
+ + +
+
+ + + + + + + + (pathInputFocused = true)} + onblur={() => (pathInputFocused = false)} + onkeydown={handleKeydown} + placeholder="/path/to/file" + spellcheck="false" + autocomplete="off" + class="flex-1 bg-transparent font-mono text-meta text-[var(--color-text-primary)] outline-none placeholder:text-[var(--color-text-muted)]" + /> + +
+
+ + +
+ {#each breadcrumbs() as crumb, i} + {#if i > 0} + + + + {/if} + + {/each} +
+ + +
+ {#if dirLoading} +
+
+ + + + Loading... +
+
+ {:else if dirError} +
+
+ + + + {dirError} +
+
+ {:else if entries.length === 0} +
+ + + + Empty directory +
+ {:else} + + {#if currentPath !== '/'} + + {/if} + + {#each sortedEntries as entry (entry.path)} + + {/each} + {/if} +
+ + + {#if !dirLoading && !dirError} +
+ + {entries.length} item{entries.length !== 1 ? 's' : ''} + +
+ {/if} +
+ + +
+ {#if !selectedFile} + +
+
+ + + + + Select a file to preview +
+
+ {:else} + +
+
+ + + + + {selectedFile.path} +
+
+ {formatFileSize(selectedFile.size)} + +
+
+ + +
+ {#if fileLoading} +
+
+ + + + Reading file... +
+
+ {:else if fileError} +
+
+ + + + {fileError} +
+
+ {:else if isBinaryFile(selectedFile.name) || isFileTooLarge(selectedFile.size) || (selectedFile && fileContent === null && !fileLoading)} + +
+
+
+ {#if isFileTooLarge(selectedFile.size)} + + + + + + {:else} + + + + + {/if} +
+
+ {#if isFileTooLarge(selectedFile.size)} + File too large to preview + + {formatFileSize(selectedFile.size)} exceeds the 10 MB preview limit + + {:else} + Binary file + + This file cannot be displayed as text + + {/if} +
+ +
+
+ {:else if fileContent !== null} + +
+
{#each fileContent.split('\n') as line, i}
{i + 1}{line}
{/each}
+
+ {/if} +
+ {/if} +
+ +
+{/if} diff --git a/frontend/src/lib/components/StatsPanel.svelte b/frontend/src/lib/components/StatsPanel.svelte index d7a2f12..19be331 100644 --- a/frontend/src/lib/components/StatsPanel.svelte +++ b/frontend/src/lib/components/StatsPanel.svelte @@ -185,7 +185,7 @@ ...BASE_CHART_OPTIONS.scales.y, ticks: { ...BASE_CHART_OPTIONS.scales.y.ticks, - callback: (v: number) => `${v}`, + callback: (v: string | number) => `${v}`, }, }, }, @@ -215,7 +215,8 @@ tooltip: { ...BASE_CHART_OPTIONS.plugins.tooltip, callbacks: { - label: (ctx: { parsed: { y: number } }) => ` ${ctx.parsed.y.toFixed(1)} GB`, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + label: (ctx: any) => ` ${ctx.parsed.y.toFixed(1)} GB`, }, }, }, @@ -225,7 +226,7 @@ ...BASE_CHART_OPTIONS.scales.y, ticks: { ...BASE_CHART_OPTIONS.scales.y.ticks, - callback: (v: number) => `${(+v).toFixed(1)} GB`, + callback: (v: string | number) => `${(+v).toFixed(1)} GB`, }, }, }, diff --git a/frontend/src/routes/admin/templates/+page.svelte b/frontend/src/routes/admin/templates/+page.svelte index 4619e7b..a15bf96 100644 --- a/frontend/src/routes/admin/templates/+page.svelte +++ b/frontend/src/routes/admin/templates/+page.svelte @@ -1,5 +1,6 @@ @@ -230,7 +262,11 @@ background-color: var(--color-bg-3); } .file-row.active { - background-color: rgba(94, 140, 88, 0.08); + background-color: var(--color-accent-glow); + border-left: 2px solid var(--color-accent); + } + .file-row:not(.active) { + border-left: 2px solid transparent; } .preview-code { @@ -238,23 +274,21 @@ -moz-tab-size: 4; } - /* Thin scrollbar for file tree and preview */ - .thin-scroll::-webkit-scrollbar { width: 6px; height: 6px; } - .thin-scroll::-webkit-scrollbar-track { background: transparent; } - .thin-scroll::-webkit-scrollbar-thumb { - background: var(--color-bg-5); - border-radius: 3px; + /* Staggered row entrance */ + @keyframes rowSlideIn { + from { opacity: 0; transform: translateX(-4px); } + to { opacity: 1; transform: translateX(0); } } - .thin-scroll::-webkit-scrollbar-thumb:hover { - background: var(--color-text-muted); + .row-enter { + animation: rowSlideIn 0.15s ease both; } - @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } + /* Line highlight on hover */ + .code-line:hover .line-content { + background-color: var(--color-bg-3); } - .fade-in { - animation: fadeIn 0.2s ease both; + .code-line:hover .line-num { + color: var(--color-text-tertiary); } @@ -264,50 +298,52 @@ - File browser is only available for running capsules. + File browser requires a running capsule. {:else}
-
+
- - - - - - - + ? 'border-[var(--color-accent)]/50 bg-[var(--color-bg-0)]' + : 'border-[var(--color-border)] bg-[var(--color-bg-1)]'}"> + + (pathInputFocused = true)} onblur={() => (pathInputFocused = false)} onkeydown={handleKeydown} - placeholder="/path/to/file" + placeholder="Enter path..." spellcheck="false" autocomplete="off" class="flex-1 bg-transparent font-mono text-meta text-[var(--color-text-primary)] outline-none placeholder:text-[var(--color-text-muted)]" />
-
+
{#each breadcrumbs() as crumb, i} {#if i > 0} @@ -316,16 +352,25 @@ {/if} {/each}
-
+
{#if dirLoading}
@@ -345,11 +390,13 @@
{:else if entries.length === 0} -
- - - - Empty directory +
+
+ + + +
+ Nothing here yet
{:else} @@ -365,11 +412,12 @@ {/if} - {#each sortedEntries as entry (entry.path)} + {#each sortedEntries as entry, idx (entry.path)}
- {#if !dirLoading && !dirError} -
- - {entries.length} item{entries.length !== 1 ? 's' : ''} - + {#if !dirLoading && !dirError && entries.length > 0} +
+ {#if dirCount > 0} + + {dirCount} dir{dirCount !== 1 ? 's' : ''} + + {/if} + {#if fileCount > 0} + + {fileCount} file{fileCount !== 1 ? 's' : ''} + + {/if}
{/if}
@@ -435,24 +490,41 @@
- - - - - Select a file to preview +
+ + + + +
+
+ No file selected + Choose a file from the tree, or enter a path directly +
{:else} -
+
- - - - + {#if isBinaryFile(selectedFile.name) || isFileTooLarge(selectedFile.size)} + + + + + {:else} + + + + + {/if} {selectedFile.path} + {#if fileExt(selectedFile.name)} + + {fileExt(selectedFile.name)} + + {/if}
-
+
{formatFileSize(selectedFile.size)}
{:else if fileContent !== null} -
-
{#each fileContent.split('\n') as line, i}
{i + 1}{line}
{/each}
+
+
{#each fileContent.split('\n') as line, i}
{i + 1}{line || ' '}
{/each}
{/if}
diff --git a/frontend/src/routes/admin/templates/+page.svelte b/frontend/src/routes/admin/templates/+page.svelte index a15bf96..0a4703c 100644 --- a/frontend/src/routes/admin/templates/+page.svelte +++ b/frontend/src/routes/admin/templates/+page.svelte @@ -419,7 +419,7 @@
{tmpl.name} - +
From 4ed17b27763c8ac5ae5101505a3d317548126b3a Mon Sep 17 00:00:00 2001 From: pptx704 Date: Fri, 10 Apr 2026 19:23:48 +0600 Subject: [PATCH 4/5] Fix stale WRENN_SANDBOX_ID and WRENN_TEMPLATE_ID after snapshot restore After restoring a VM from snapshot, envd had already completed its initial MMDS poll, so the metadata files in /run/wrenn/ and env vars retained values from the original sandbox. Call POST /init after WaitUntilReady on both resume and create-from-template paths to trigger envd to re-read MMDS. --- internal/envdclient/client.go | 24 ++++++++++++++++++++++++ internal/sandbox/manager.go | 10 ++++++++++ 2 files changed, 34 insertions(+) diff --git a/internal/envdclient/client.go b/internal/envdclient/client.go index d20ffd5..0567cf1 100644 --- a/internal/envdclient/client.go +++ b/internal/envdclient/client.go @@ -268,6 +268,30 @@ func (c *Client) ReadFile(ctx context.Context, path string) ([]byte, error) { return data, nil } +// PostInit calls envd's POST /init endpoint, which triggers a re-read of +// Firecracker MMDS metadata. This updates WRENN_SANDBOX_ID, WRENN_TEMPLATE_ID +// env vars and the corresponding files under /run/wrenn/ inside the guest. +// Must be called after snapshot restore so envd picks up the new sandbox's metadata. +func (c *Client) PostInit(ctx context.Context) error { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.base+"/init", nil) + if err != nil { + return fmt.Errorf("create request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("post init: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("post init: status %d: %s", resp.StatusCode, string(body)) + } + + return nil +} + // ListDir lists directory contents inside the sandbox. func (c *Client) ListDir(ctx context.Context, path string, depth uint32) (*envdpb.ListDirResponse, error) { req := connect.NewRequest(&envdpb.ListDirRequest{ diff --git a/internal/sandbox/manager.go b/internal/sandbox/manager.go index e920476..74df2a2 100644 --- a/internal/sandbox/manager.go +++ b/internal/sandbox/manager.go @@ -697,6 +697,11 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int) return nil, fmt.Errorf("wait for envd: %w", err) } + // Trigger envd to re-read MMDS so it picks up the new sandbox/template IDs. + if err := client.PostInit(waitCtx); err != nil { + slog.Warn("post-init failed after resume, metadata files may be stale", "sandbox", sandboxID, "error", err) + } + now := time.Now() sb := &sandboxState{ Sandbox: models.Sandbox{ @@ -1098,6 +1103,11 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team return nil, fmt.Errorf("wait for envd: %w", err) } + // Trigger envd to re-read MMDS so it picks up the new sandbox/template IDs. + if err := client.PostInit(waitCtx); err != nil { + slog.Warn("post-init failed after template restore, metadata files may be stale", "sandbox", sandboxID, "error", err) + } + now := time.Now() sb := &sandboxState{ Sandbox: models.Sandbox{ From 851f54a9e18dee145c4294d58fcdd9b78a2b71c1 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Fri, 10 Apr 2026 19:24:24 +0600 Subject: [PATCH 5/5] Polish file browser: add up button, normalize design, improve UX Add parent directory button in breadcrumb bar, remove redundant .. row from file list. Normalize styles to use design system tokens (accent glow, iconFloat, fadeUp). Improve empty states, add staggered row entrance animation, file extension badge, and clearer UX copy. --- frontend/src/lib/components/FilesTab.svelte | 32 ++++++++++++--------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/frontend/src/lib/components/FilesTab.svelte b/frontend/src/lib/components/FilesTab.svelte index 7fcb98e..6baaf7e 100644 --- a/frontend/src/lib/components/FilesTab.svelte +++ b/frontend/src/lib/components/FilesTab.svelte @@ -56,6 +56,8 @@ const dirCount = $derived(entries.filter((e) => e.type === 'directory').length); const fileCount = $derived(entries.filter((e) => e.type !== 'directory').length); + const canGoUp = $derived(currentPath !== '/' && currentPath.startsWith('/')); + async function navigateTo(path: string) { currentPath = normalizePath(path); pathInput = currentPath; @@ -343,7 +345,22 @@ -
+
+ + + {#each breadcrumbs() as crumb, i} {#if i > 0} @@ -399,19 +416,6 @@ Nothing here yet
{:else} - - {#if currentPath !== '/'} - - {/if} - {#each sortedEntries as entry, idx (entry.path)}