kenn-io--agentsview
f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
178 行
4.4 KiB
Go
178 行
4.4 KiB
Go
package importer
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// AssetIndex maps asset pointer prefixes to file paths on disk.
|
|
type AssetIndex struct {
|
|
entries map[string]string
|
|
}
|
|
|
|
// BuildAssetIndex scans an export directory for image assets.
|
|
// ChatGPT exports store images in three locations:
|
|
// - dalle-generations/ (DALL-E outputs)
|
|
// - user-*/ directories (user uploads via sediment://)
|
|
// - root directory as file-* (user uploads via file-service://)
|
|
func BuildAssetIndex(exportDir string) AssetIndex {
|
|
idx := AssetIndex{entries: make(map[string]string)}
|
|
|
|
// DALL-E generated images.
|
|
dalleDir := filepath.Join(exportDir, "dalle-generations")
|
|
scanAssetDir(dalleDir, idx.entries)
|
|
|
|
// User uploads in user-*/ subdirectories.
|
|
matches, _ := filepath.Glob(filepath.Join(exportDir, "user-*"))
|
|
for _, m := range matches {
|
|
info, err := os.Stat(m)
|
|
if err == nil && info.IsDir() {
|
|
scanAssetDir(m, idx.entries)
|
|
}
|
|
}
|
|
|
|
// User uploads as loose file-* in the root directory.
|
|
scanAssetFiles(exportDir, idx.entries)
|
|
|
|
return idx
|
|
}
|
|
|
|
func scanAssetDir(dir string, entries map[string]string) {
|
|
files, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, f := range files {
|
|
if f.IsDir() {
|
|
continue
|
|
}
|
|
name := f.Name()
|
|
prefix := extractAssetPrefix(name)
|
|
if prefix != "" {
|
|
entries[prefix] = filepath.Join(dir, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// scanAssetFiles indexes only file-* and file_* entries in a
|
|
// directory. Used for the root dir where non-asset files exist.
|
|
func scanAssetFiles(dir string, entries map[string]string) {
|
|
files, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, f := range files {
|
|
if f.IsDir() {
|
|
continue
|
|
}
|
|
name := f.Name()
|
|
if !strings.HasPrefix(name, "file-") &&
|
|
!strings.HasPrefix(name, "file_") {
|
|
continue
|
|
}
|
|
prefix := extractAssetPrefix(name)
|
|
if prefix != "" {
|
|
entries[prefix] = filepath.Join(dir, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// extractAssetPrefix gets the stable prefix from filenames like
|
|
// "file-abc123-aaaa-bbbb-cccc-dddddddddddd.webp"
|
|
func extractAssetPrefix(name string) string {
|
|
base := strings.TrimSuffix(name, filepath.Ext(name))
|
|
// UUID is 36 chars at end, preceded by hyphen = 37 chars total
|
|
if len(base) > 37 && base[len(base)-37] == '-' {
|
|
return base[:len(base)-37]
|
|
}
|
|
return base
|
|
}
|
|
|
|
// Resolve maps an asset pointer URL to a file path.
|
|
func (idx AssetIndex) Resolve(pointer string) (string, bool) {
|
|
var prefix string
|
|
switch {
|
|
case strings.HasPrefix(pointer, "file-service://"):
|
|
prefix = strings.TrimPrefix(pointer, "file-service://")
|
|
case strings.HasPrefix(pointer, "sediment://"):
|
|
prefix = strings.TrimPrefix(pointer, "sediment://")
|
|
default:
|
|
return "", false
|
|
}
|
|
path, ok := idx.entries[prefix]
|
|
return path, ok
|
|
}
|
|
|
|
// allowedImageExts is the set of passive image formats that
|
|
// are safe to serve inline. Active content (svg, html, js) is
|
|
// rejected to prevent stored XSS.
|
|
var allowedImageExts = map[string]bool{
|
|
".png": true,
|
|
".jpg": true,
|
|
".jpeg": true,
|
|
".webp": true,
|
|
".gif": true,
|
|
}
|
|
|
|
// CopyAsset copies a file to the assets directory using its
|
|
// SHA-256 hash as the filename. Returns the asset:// reference.
|
|
// Only passive image types are accepted; active content is
|
|
// rejected.
|
|
func CopyAsset(srcPath, assetsDir string) (string, error) {
|
|
ext := strings.ToLower(filepath.Ext(srcPath))
|
|
if !allowedImageExts[ext] {
|
|
return "", fmt.Errorf(
|
|
"unsupported asset type: %s", ext,
|
|
)
|
|
}
|
|
|
|
f, err := os.Open(srcPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("reading asset: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
h := sha256.New()
|
|
if _, err := io.Copy(h, f); err != nil {
|
|
return "", fmt.Errorf("hashing asset: %w", err)
|
|
}
|
|
|
|
hash := fmt.Sprintf("%x", h.Sum(nil))
|
|
filename := hash + ext
|
|
destPath := filepath.Join(assetsDir, filename)
|
|
|
|
if _, err := os.Stat(destPath); err == nil {
|
|
return "asset://" + filename, nil
|
|
}
|
|
|
|
if err := os.MkdirAll(assetsDir, 0o755); err != nil {
|
|
return "", fmt.Errorf("creating assets dir: %w", err)
|
|
}
|
|
|
|
// Re-read source for copy (we consumed it for hashing).
|
|
src, err := os.Open(srcPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("reopening asset: %w", err)
|
|
}
|
|
defer src.Close()
|
|
|
|
out, err := os.Create(destPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("writing asset: %w", err)
|
|
}
|
|
|
|
if _, err := io.Copy(out, src); err != nil {
|
|
out.Close()
|
|
return "", fmt.Errorf("copying asset: %w", err)
|
|
}
|
|
if err := out.Close(); err != nil {
|
|
return "", fmt.Errorf("closing asset: %w", err)
|
|
}
|
|
|
|
return "asset://" + filename, nil
|
|
}
|