@strapi/data-transfer
Version:
Data transfer capabilities for Strapi
175 lines (171 loc) • 6.49 kB
JavaScript
;
var path = require('path');
var stream = require('stream');
var fp = require('lodash/fp');
var fs = require('fs-extra');
var mimeTypes = require('mime-types');
const PROJECT_SETTINGS_CORE_STORE_KEY = 'core_admin_project-settings';
// The shape persisted back to core-store, matching what the admin project-settings
// service stores (so the admin UI keeps behaving identically after a transfer).
const LOGO_PERSISTED_FIELDS = [
'name',
'hash',
'url',
'width',
'height',
'ext',
'size',
'provider'
];
const isProjectSettingsRow = (row)=>row.key === PROJECT_SETTINGS_CORE_STORE_KEY;
const getConfiguredUploadProvider = (strapi)=>strapi.config.get('plugin::upload')?.provider;
// Admin logos with no provider were uploaded with the local provider (the default).
const isLocalProviderLogo = (logo)=>!logo.provider || logo.provider === 'local';
// Best-effort mime type for a logo so that remote providers (S3, Cloudinary, …)
// store the correct Content-Type and the image renders instead of downloading.
// The admin does not persist the mime type, so it is reconstructed from the
// file extension (falling back to the file name).
const getLogoMimeType = (logo)=>{
const fromExt = logo.ext ? mimeTypes.lookup(logo.ext) : false;
if (fromExt) {
return fromExt;
}
const fromName = logo.name ? mimeTypes.lookup(logo.name) : false;
if (fromName) {
return fromName;
}
return undefined;
};
/**
* Resolve the URL to read a non-local logo from during export.
*
* Mirrors the media-library asset export: when the file lives on the
* currently-configured provider and that provider serves private files,
* a signed URL is generated so the bytes can be fetched.
*/ const getRemoteLogoUrl = async (strapi, logo)=>{
const { provider } = strapi.plugins.upload;
const providerName = getConfiguredUploadProvider(strapi);
if (logo.provider === providerName && typeof provider?.isPrivate === 'function' && typeof provider?.getSignedUrl === 'function' && await provider.isPrivate()) {
const { url } = await provider.getSignedUrl(logo);
return url;
}
return logo.url;
};
const logoLabel = (logo)=>logo.name ?? logo.hash ?? 'unknown';
const readLocalLogoBuffer = async (strapi, logo)=>{
const filepath = path.join(strapi.dirs.static.public, logo.url);
try {
const buffer = await fs.readFile(filepath);
return buffer.toString('base64');
} catch (error) {
const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
if (code === 'ENOENT') {
strapi.log.warn(`[Data transfer] Admin logo "${logoLabel(logo)}" exists in project settings but no corresponding file was found to transfer. Path: ${filepath}`);
return undefined;
}
throw error;
}
};
const readRemoteLogoBuffer = async (strapi, logo)=>{
const url = await getRemoteLogoUrl(strapi, logo);
const response = await strapi.fetch(url);
if (response.status !== 200 || !response.body) {
strapi.log.warn(`[Data transfer] Admin logo "${logoLabel(logo)}" exists in project settings but could not be fetched for transfer. URL: ${url} (status: ${response.status})`);
return undefined;
}
const chunks = [];
for await (const chunk of stream.Readable.fromWeb(response.body)){
chunks.push(chunk);
}
return Buffer.concat(chunks).toString('base64');
};
const readLogoTransferBuffer = async (strapi, logo)=>{
if (!logo.url) {
return undefined;
}
if (isLocalProviderLogo(logo)) {
return readLocalLogoBuffer(strapi, logo);
}
return readRemoteLogoBuffer(strapi, logo);
};
const enrichLogoForExport = async (strapi, logo)=>{
if (!logo?.url) {
return logo;
}
const transferBuffer = await readLogoTransferBuffer(strapi, logo);
if (!transferBuffer) {
return logo;
}
return {
...logo,
__transferBuffer: transferBuffer
};
};
const enrichProjectSettingsForExport = async (strapi, row)=>{
if (!isProjectSettingsRow(row)) {
return row;
}
const settings = row.value;
const [menuLogo, authLogo] = await Promise.all([
enrichLogoForExport(strapi, settings.menuLogo),
enrichLogoForExport(strapi, settings.authLogo)
]);
return {
...row,
value: {
...settings,
menuLogo,
authLogo
}
};
};
/**
* Re-upload a transferred logo to the destination's configured upload provider.
*
* The file is always re-uploaded through the destination provider, so the
* persisted `url` and `provider` reflect the destination (matching how the
* media-library asset restore behaves). A best-effort mime type is supplied so
* remote providers serve the logo with the correct Content-Type.
*/ const uploadLogoFromTransferBuffer = async (strapi, logo)=>{
if (!logo.__transferBuffer) {
return logo;
}
const { __transferBuffer: transferBuffer, ...logoWithoutBuffer } = logo;
const provider = getConfiguredUploadProvider(strapi);
const mime = getLogoMimeType(logoWithoutBuffer);
const file = {
...logoWithoutBuffer,
...mime ? {
mime
} : {},
stream: stream.Readable.from(Buffer.from(transferBuffer, 'base64')),
provider
};
await strapi.plugin('upload').provider.uploadStream(file);
return fp.pick(LOGO_PERSISTED_FIELDS, file);
};
const restoreProjectSettingsLogos = async (strapi, settings)=>{
const [menuLogo, authLogo] = await Promise.all([
settings.menuLogo ? uploadLogoFromTransferBuffer(strapi, settings.menuLogo) : settings.menuLogo,
settings.authLogo ? uploadLogoFromTransferBuffer(strapi, settings.authLogo) : settings.authLogo
]);
return {
...settings,
menuLogo,
authLogo
};
};
const restoreProjectSettingsRow = async (strapi, row)=>{
if (!isProjectSettingsRow(row)) {
return row;
}
return {
...row,
value: await restoreProjectSettingsLogos(strapi, row.value)
};
};
exports.PROJECT_SETTINGS_CORE_STORE_KEY = PROJECT_SETTINGS_CORE_STORE_KEY;
exports.enrichProjectSettingsForExport = enrichProjectSettingsForExport;
exports.restoreProjectSettingsLogos = restoreProjectSettingsLogos;
exports.restoreProjectSettingsRow = restoreProjectSettingsRow;
//# sourceMappingURL=project-settings-logos.js.map