fix(opencode): enforce storage path invariants (#29666)
This commit is contained in:
1
packages/core/src/database/migration.gen.ts
generated
1
packages/core/src/database/migration.gen.ts
generated
@@ -23,5 +23,6 @@ export const migrations = (
|
||||
import("./migration/20260510033149_session_usage"),
|
||||
import("./migration/20260511000411_data_migration_state"),
|
||||
import("./migration/20260511173437_session-metadata"),
|
||||
import("./migration/20260601010001_normalize_storage_paths"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260601010001_normalize_storage_paths",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`UPDATE project SET worktree = REPLACE(worktree, char(92), '/') WHERE worktree GLOB '[A-Za-z]:' || char(92) || '*' OR worktree LIKE char(92) || char(92) || '%';`)
|
||||
yield* tx.run(`UPDATE project SET sandboxes = REPLACE(sandboxes, char(92) || char(92), '/') WHERE instr(sandboxes, char(92)) > 0 AND (worktree GLOB '[A-Za-z]:*' OR worktree LIKE '//%');`)
|
||||
yield* tx.run(`UPDATE session SET directory = REPLACE(directory, char(92), '/') WHERE directory GLOB '[A-Za-z]:' || char(92) || '*' OR directory LIKE char(92) || char(92) || '%';`)
|
||||
yield* tx.run(`UPDATE session SET path = REPLACE(path, char(92), '/') WHERE path IS NOT NULL AND instr(path, char(92)) > 0 AND (directory GLOB '[A-Za-z]:*' OR directory LIKE '//%');`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
91
packages/core/src/database/path.ts
Normal file
91
packages/core/src/database/path.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import nodePath from "path"
|
||||
import { customType } from "drizzle-orm/sqlite-core"
|
||||
import { AbsolutePath } from "../schema"
|
||||
|
||||
function storagePath(input: string) {
|
||||
if (process.platform !== "win32") return input
|
||||
return input.replaceAll("\\", "/")
|
||||
}
|
||||
|
||||
function isWindowsStoragePath(input: string) {
|
||||
return /^[A-Za-z]:\//.test(input) || input.startsWith("//")
|
||||
}
|
||||
|
||||
function absolute(input: string) {
|
||||
const result = storagePath(input)
|
||||
if (!nodePath.posix.isAbsolute(result) && !(process.platform === "win32" && isWindowsStoragePath(result))) {
|
||||
throw new Error(`Path is not absolute: ${input}`)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function toPlatform(input: string) {
|
||||
if (process.platform !== "win32" || !isWindowsStoragePath(input)) return input
|
||||
return input.replaceAll("/", "\\")
|
||||
}
|
||||
|
||||
export const absoluteColumn = customType<{
|
||||
data: AbsolutePath
|
||||
driverData: string
|
||||
driverOutput: string
|
||||
}>({
|
||||
dataType() {
|
||||
return "text"
|
||||
},
|
||||
toDriver(input) {
|
||||
return absolute(input)
|
||||
},
|
||||
fromDriver(input) {
|
||||
return AbsolutePath.make(toPlatform(absolute(input)))
|
||||
},
|
||||
})
|
||||
|
||||
// Legacy sessions may persist an empty directory. Keep that existing value
|
||||
// readable while normalizing and validating every real directory.
|
||||
export const directoryColumn = customType<{
|
||||
data: string
|
||||
driverData: string
|
||||
driverOutput: string
|
||||
}>({
|
||||
dataType() {
|
||||
return "text"
|
||||
},
|
||||
toDriver(input) {
|
||||
return input ? absolute(input) : input
|
||||
},
|
||||
fromDriver(input) {
|
||||
return input ? toPlatform(absolute(input)) : input
|
||||
},
|
||||
})
|
||||
|
||||
export const pathColumn = customType<{
|
||||
data: string
|
||||
driverData: string
|
||||
driverOutput: string
|
||||
}>({
|
||||
dataType() {
|
||||
return "text"
|
||||
},
|
||||
toDriver(input) {
|
||||
return storagePath(input)
|
||||
},
|
||||
fromDriver(input) {
|
||||
return storagePath(input)
|
||||
},
|
||||
})
|
||||
|
||||
export const absoluteArrayColumn = customType<{
|
||||
data: AbsolutePath[]
|
||||
driverData: string
|
||||
driverOutput: string
|
||||
}>({
|
||||
dataType() {
|
||||
return "text"
|
||||
},
|
||||
toDriver(input) {
|
||||
return JSON.stringify(input.map(absolute))
|
||||
},
|
||||
fromDriver(input) {
|
||||
return (JSON.parse(input) as string[]).map((item) => AbsolutePath.make(toPlatform(absolute(item))))
|
||||
},
|
||||
})
|
||||
@@ -1,10 +1,11 @@
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
||||
import * as DatabasePath from "../database/path"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
import { ProjectV2 } from "../project"
|
||||
|
||||
export const ProjectTable = sqliteTable("project", {
|
||||
id: text().$type<ProjectV2.ID>().primaryKey(),
|
||||
worktree: text().notNull(),
|
||||
worktree: DatabasePath.absoluteColumn().notNull(),
|
||||
vcs: text(),
|
||||
name: text(),
|
||||
icon_url: text(),
|
||||
@@ -12,6 +13,6 @@ export const ProjectTable = sqliteTable("project", {
|
||||
icon_color: text(),
|
||||
...Timestamps,
|
||||
time_initialized: integer(),
|
||||
sandboxes: text({ mode: "json" }).notNull().$type<string[]>(),
|
||||
sandboxes: DatabasePath.absoluteArrayColumn().notNull(),
|
||||
commands: text({ mode: "json" }).$type<{ start?: string }>(),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sqliteTable, text, integer, index, primaryKey, real } from "drizzle-orm/sqlite-core"
|
||||
import * as DatabasePath from "../database/path"
|
||||
import { ProjectTable } from "../project/sql"
|
||||
import type { SessionMessage } from "./message"
|
||||
import type { Snapshot } from "../snapshot"
|
||||
@@ -24,8 +25,8 @@ export const SessionTable = sqliteTable(
|
||||
workspace_id: text().$type<WorkspaceV2.ID>(),
|
||||
parent_id: text().$type<SessionSchema.ID>(),
|
||||
slug: text().notNull(),
|
||||
directory: text().notNull(),
|
||||
path: text(),
|
||||
directory: DatabasePath.directoryColumn().notNull(),
|
||||
path: DatabasePath.pathColumn(),
|
||||
title: text().notNull(),
|
||||
version: text().notNull(),
|
||||
share_url: text(),
|
||||
|
||||
Reference in New Issue
Block a user