lib/project-image.js
Rama main · réplica de lectura del repositorio original.
var fs = require("fs");
var path = require("path");
var crypto = require("crypto");
var execFileSync = require("child_process").execFileSync;
/**
* Attach image handling to a project context.
*
* ctx fields:
* cwd, slug
*/
function attachImage(ctx) {
var cwd = ctx.cwd;
var slug = ctx.slug;
var fsAsUser = ctx.fsAsUser;
// --- Chat image storage ---
var _imgConfig = require("./config");
var _imgUtils = require("./utils");
var _imagesBaseDir = path.join(_imgConfig.CONFIG_DIR, "images");
var _imagesEncodedCwd = _imgUtils.encodeCwd(cwd);
var imagesDir = ctx.imagesDir || path.join(_imagesBaseDir, _imagesEncodedCwd);
function isWithin(base, candidate) {
return candidate === base || candidate.indexOf(base + path.sep) === 0;
}
function saveGeneratedImageToProject(fileName, requestedPath, overwrite, osUserInfo) {
if (typeof fileName !== "string" || !/^\d+-[a-f0-9]+\.\w+$/.test(fileName)) {
return { ok: false, status: 400, error: "Invalid generated image" };
}
if (typeof requestedPath !== "string" || !requestedPath.trim() || requestedPath.length > 500) {
return { ok: false, status: 400, error: "Enter a project path" };
}
var sourcePath = path.join(imagesDir, fileName);
var sourceExt = path.extname(fileName).toLowerCase();
var targetPath = requestedPath.trim();
if (path.isAbsolute(targetPath)) {
return { ok: false, status: 400, error: "Use a project-relative path" };
}
var targetExt = path.extname(targetPath).toLowerCase();
if (targetExt !== sourceExt) {
return { ok: false, status: 400, error: "Keep the " + sourceExt + " file extension" };
}
var projectRoot;
var targetAbs;
try {
projectRoot = fs.realpathSync(cwd);
targetAbs = path.resolve(projectRoot, targetPath);
} catch (e) {
return { ok: false, status: 400, error: "Project path is not available" };
}
if (targetAbs === projectRoot || !isWithin(projectRoot, targetAbs)) {
return { ok: false, status: 403, error: "Path must stay inside the project" };
}
try {
var sourceReal = fs.realpathSync(sourcePath);
var imagesRoot = fs.realpathSync(imagesDir);
if (!isWithin(imagesRoot, sourceReal) || !fs.statSync(sourceReal).isFile()) {
return { ok: false, status: 404, error: "Generated image not found" };
}
var parentDir = path.dirname(targetAbs);
var existingParent = parentDir;
while (!fs.existsSync(existingParent) && existingParent !== projectRoot) {
existingParent = path.dirname(existingParent);
}
var realExistingParent = fs.realpathSync(existingParent);
if (!isWithin(projectRoot, realExistingParent)) {
return { ok: false, status: 403, error: "Path must stay inside the project" };
}
if (osUserInfo && typeof fsAsUser === "function") {
fsAsUser("mkdir", { dir: parentDir }, osUserInfo);
} else {
fs.mkdirSync(parentDir, { recursive: true });
}
var realParent = fs.realpathSync(parentDir);
if (!isWithin(projectRoot, realParent)) {
return { ok: false, status: 403, error: "Path must stay inside the project" };
}
if (fs.existsSync(targetAbs)) {
if (fs.lstatSync(targetAbs).isSymbolicLink()) {
return { ok: false, status: 403, error: "Symbolic link targets cannot be replaced" };
}
if (!overwrite) {
return { ok: false, status: 409, error: "A file already exists at this path" };
}
}
if (osUserInfo && typeof fsAsUser === "function") {
fsAsUser("copy", { source: sourceReal, file: targetAbs, overwrite: !!overwrite }, osUserInfo);
} else {
fs.copyFileSync(sourceReal, targetAbs, overwrite ? 0 : fs.constants.COPYFILE_EXCL);
}
try { fs.chmodSync(targetAbs, 0o644); } catch (e2) {}
return { ok: true, path: path.relative(projectRoot, targetAbs).split(path.sep).join("/") };
} catch (e) {
if (e && e.code === "EEXIST") {
return { ok: false, status: 409, error: "A file already exists at this path" };
}
return { ok: false, status: 500, error: "Could not save the generated image" };
}
}
// Convert imageRefs in history entries to images with URLs for the client
function hydrateImageRefs(entry) {
if (!entry) return entry;
// Hydrate context_preview: convert screenshotFile to screenshotUrl
if (entry.type === "context_preview" && entry.tab && entry.tab.screenshotFile) {
var hydrated = {};
for (var k in entry) hydrated[k] = entry[k];
hydrated.tab = {};
for (var tk in entry.tab) hydrated.tab[tk] = entry.tab[tk];
hydrated.tab.screenshotUrl = "/p/" + slug + "/images/" + entry.tab.screenshotFile;
delete hydrated.tab.screenshotFile;
return hydrated;
}
if (!entry.imageRefs) return entry;
if (entry.type !== "user_message" && entry.type !== "mention_user" && entry.type !== "user_mention" && entry.type !== "generated_image") return entry;
var images = [];
for (var ri = 0; ri < entry.imageRefs.length; ri++) {
var ref = entry.imageRefs[ri];
images.push({ mediaType: ref.mediaType, url: "/p/" + slug + "/images/" + ref.file });
}
var hydrated = {};
for (var k2 in entry) {
if (k2 !== "imageRefs") hydrated[k2] = entry[k2];
}
hydrated.images = images;
return hydrated;
}
function saveImageFile(mediaType, base64data, ownerLinuxUser) {
try { fs.mkdirSync(imagesDir, { recursive: true }); } catch (e) {}
var ext = mediaType === "image/png" ? ".png" : mediaType === "image/gif" ? ".gif" : mediaType === "image/webp" ? ".webp" : ".jpg";
var hash = crypto.createHash("sha256").update(base64data).digest("hex").substring(0, 16);
var fileName = Date.now() + "-" + hash + ext;
var filePath = path.join(imagesDir, fileName);
try {
fs.writeFileSync(filePath, Buffer.from(base64data, "base64"));
if (process.platform !== "win32") {
// 644 so all local users can read (needed for git, copy, etc.)
try { fs.chmodSync(filePath, 0o644); } catch (e) {}
// In OS-user mode the daemon runs as root, so chown the file
// (and parent dirs) to the session owner to avoid permission issues.
if (ownerLinuxUser) {
try {
var osUsersMod = require("./os-users");
var uid = osUsersMod.getLinuxUserUid(ownerLinuxUser);
if (uid != null) {
execFileSync("chown", [String(uid), filePath]);
// Also fix parent dirs if root-owned
try {
var dirStat = fs.statSync(imagesDir);
if (dirStat.uid !== uid) {
execFileSync("chown", [String(uid), imagesDir]);
}
} catch (e2) {}
}
} catch (e) {}
}
}
return fileName;
} catch (e) {
console.error("[images] Failed to save image:", e.message);
return null;
}
}
return {
imagesDir: imagesDir,
hydrateImageRefs: hydrateImageRefs,
saveImageFile: saveImageFile,
saveGeneratedImageToProject: saveGeneratedImageToProject,
};
}
module.exports = { attachImage: attachImage };