Files
supabase/apps/studio/lib/api/self-hosted/functions/fileSystemStore.ts
Kalleby Santos 997203cd64 feat(studio-local): functions management api - function blob artifacts (#42349)
## What kind of change does this PR introduce?

Feature

## What is the current behavior?

Functions page on self-hosted differs from Platform

## What is the new behavior?

> [!NOTE]  
> This PR only add readonly operations. Function edit and deploy should
be implemented in a future one.

Adds the possibility to download and see function code in Self-Host
version.

<details>

<img width="1465" height="944" alt="image"
src="https://github.com/user-attachments/assets/4bbf8f5c-3390-4de6-9e8b-8ec9cd59ebad"
/>

</details>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* API endpoint to stream function files/artifacts as
multipart/form-data.
* New function file entry type and server-side file listing for
functions.

* **Improvements**
  * Edge Functions "Code" navigation item always visible.
* Download popover reworked: ZIP download always available; CLI section
shown only on supported platforms.
* Editor set to read-only and file actions disabled on unsupported
environments.

* **Editor**
* Added JavaScript, TypeScript, and Markdown language modules for the
embedded editor.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
2026-02-06 20:06:37 +00:00

81 lines
2.6 KiB
TypeScript

import type { Dirent } from 'node:fs'
import { readdir, stat } from 'node:fs/promises'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import { FunctionArtifact, FunctionFileEntry } from './types'
export class FileSystemFunctionsArtifactStore {
constructor(private folderPath: string) {}
async getFunctions(): Promise<FunctionArtifact[]> {
const dirEntries = await readdir(this.folderPath, { withFileTypes: true })
const functionsFolders = dirEntries.filter((dir) => dir.isDirectory() && dir.name !== 'main')
const functionsArtifacts = await Promise.all(
functionsFolders.map(parseFolderToFunctionArtifact)
)
return functionsArtifacts.filter((f) => f !== undefined)
}
async getFunctionBySlug(slug: string): Promise<FunctionArtifact | undefined> {
const dirEntries = await readdir(this.folderPath, { withFileTypes: true })
const functionFolder = dirEntries.find(
(dir) => dir.isDirectory() && dir.name !== 'main' && dir.name === slug
)
if (!functionFolder) return
return parseFolderToFunctionArtifact(functionFolder)
}
async getFileEntriesBySlug(slug: string): Promise<Array<FunctionFileEntry>> {
if (slug === 'main') return []
const functionFolderPath = path.resolve(this.folderPath, slug)
if (!functionFolderPath.startsWith(path.resolve(this.folderPath) + path.sep)) return []
const entries = await readdir(functionFolderPath, {
recursive: true,
withFileTypes: true,
})
const fileEntries = await Promise.all(
entries
.filter((entry) => entry.isFile())
.map(async (entry) => {
const absolutePath = path.join(entry.parentPath, entry.name)
const fileStat = await stat(absolutePath)
return {
absolutePath,
relativePath: path.relative(functionFolderPath, absolutePath),
size: fileStat.size,
}
})
)
return fileEntries
}
}
async function parseFolderToFunctionArtifact(
folder: Dirent
): Promise<FunctionArtifact | undefined> {
const folderPath = path.join(folder.parentPath, folder.name)
const files = await readdir(folderPath, { withFileTypes: true })
const entrypoint = files.find((file) => file.isFile() && file.name.startsWith('index'))
if (!entrypoint) return
const entrypointPath = path.join(folderPath, entrypoint.name)
const entrypointStat = await stat(entrypointPath)
return {
slug: folder.name,
entrypoint_path: pathToFileURL(entrypointPath).href,
created_at: entrypointStat.birthtimeMs,
updated_at: entrypointStat.mtimeMs,
}
}