refactor: adding logger and catch blocks
This commit is contained in:
11
.env.example
11
.env.example
@@ -4,8 +4,9 @@ SOURCE_S3_ACCESS_KEY="sirCxqNKw85GdnlLfzko"
|
|||||||
SOURCE_S3_SECRET_KEY="qbyQPjiCd5RcfIFpA3l2sG7EW9nT1gtVYkKJN08w"
|
SOURCE_S3_SECRET_KEY="qbyQPjiCd5RcfIFpA3l2sG7EW9nT1gtVYkKJN08w"
|
||||||
SOURCE_S3_BUCKET="test"
|
SOURCE_S3_BUCKET="test"
|
||||||
|
|
||||||
DEST_S3_ENDPOINT="http://95.156.253.15:9001/"
|
# ParsPack: endpoint is https://<bucket-id>.parspack.net and bucket name is the same id (e.g. c530168)
|
||||||
DEST_S3_REGION="default"
|
DEST_S3_ENDPOINT="https://c530168.parspack.net"
|
||||||
DEST_S3_ACCESS_KEY="sirCxqNKw85GdnlLfzko"
|
DEST_S3_REGION="us-west-2"
|
||||||
DEST_S3_SECRET_KEY="qbyQPjiCd5RcfIFpA3l2sG7EW9nT1gtVYkKJN08w"
|
DEST_S3_ACCESS_KEY="your-access-key"
|
||||||
DEST_S3_BUCKET="test"
|
DEST_S3_SECRET_KEY="your-secret-key"
|
||||||
|
DEST_S3_BUCKET="c530168"
|
||||||
@@ -3,16 +3,26 @@ import { DataSource } from "typeorm";
|
|||||||
import { AssetEntity } from "./entity/Assets.js";
|
import { AssetEntity } from "./entity/Assets.js";
|
||||||
import { BrandEntity } from "./entity/Brand.js";
|
import { BrandEntity } from "./entity/Brand.js";
|
||||||
import { BrandAssetEntity } from "./entity/BrandAsset.js";
|
import { BrandAssetEntity } from "./entity/BrandAsset.js";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
export const AppDataSource = new DataSource({
|
const dbConfig = {
|
||||||
type: "mysql",
|
type: "mysql",
|
||||||
host: "95.156.253.15",
|
host: "31.214.255.158",
|
||||||
port: 3306,
|
port: 3301,
|
||||||
username: "root",
|
username: "brandprod",
|
||||||
password: "Aa9923994970",
|
password: "cdcc9b7152905d21baa7758586fed2aa",
|
||||||
database: "temporary",
|
database: "brandifa_production",
|
||||||
logging: false,
|
logging: process.env.DB_LOGGING === "true",
|
||||||
entities: [AssetEntity, BrandEntity, BrandAssetEntity],
|
entities: [AssetEntity, BrandEntity, BrandAssetEntity],
|
||||||
migrations: [],
|
migrations: [],
|
||||||
subscribers: [],
|
subscribers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info("data-source", "creating AppDataSource", {
|
||||||
|
host: dbConfig.host,
|
||||||
|
port: dbConfig.port,
|
||||||
|
database: dbConfig.database,
|
||||||
|
logging: dbConfig.logging,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AppDataSource = new DataSource(dbConfig);
|
||||||
|
|||||||
302
src/index.js
302
src/index.js
@@ -1,4 +1,4 @@
|
|||||||
import "dotenv/config"
|
import "dotenv/config";
|
||||||
import {
|
import {
|
||||||
GetObjectCommand,
|
GetObjectCommand,
|
||||||
HeadObjectCommand,
|
HeadObjectCommand,
|
||||||
@@ -9,11 +9,32 @@ import { AppDataSource } from "./data-source.js";
|
|||||||
import { AssetEntity } from "./entity/Assets.js";
|
import { AssetEntity } from "./entity/Assets.js";
|
||||||
import { BrandEntity } from "./entity/Brand.js";
|
import { BrandEntity } from "./entity/Brand.js";
|
||||||
import { BrandAssetEntity } from "./entity/BrandAsset.js";
|
import { BrandAssetEntity } from "./entity/BrandAsset.js";
|
||||||
|
import { logger, runStep } from "./logger.js";
|
||||||
|
|
||||||
const AVATAR_PREFIX = "avatar";
|
const AVATAR_PREFIX = "avatar";
|
||||||
const THUMBNAIL_PREFIX = "lg";
|
const THUMBNAIL_PREFIX = "lg";
|
||||||
const DRY_RUN = process.env.DRY_RUN === "true";
|
const DRY_RUN = process.env.DRY_RUN === "true";
|
||||||
|
|
||||||
|
function resolveParsPackBucket(endpoint, bucket) {
|
||||||
|
const match = endpoint?.match(/https?:\/\/(c\d+)\.parspack\.net/i);
|
||||||
|
if (!match) return bucket;
|
||||||
|
|
||||||
|
const bucketId = match[1];
|
||||||
|
if (bucket !== bucketId) {
|
||||||
|
logger.warn(
|
||||||
|
"resolveParsPackBucket",
|
||||||
|
`ParsPack bucket name must match the endpoint id; using "${bucketId}" instead of "${bucket}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucketId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeParsPackEndpoint(endpoint) {
|
||||||
|
if (!endpoint?.includes("parspack.net")) return endpoint;
|
||||||
|
return endpoint.replace(/^http:/i, "https:").replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
const SOURCE_S3 = {
|
const SOURCE_S3 = {
|
||||||
endpoint: process.env.SOURCE_S3_ENDPOINT ?? "http://95.156.253.15:9001/",
|
endpoint: process.env.SOURCE_S3_ENDPOINT ?? "http://95.156.253.15:9001/",
|
||||||
region: process.env.SOURCE_S3_REGION ?? "default",
|
region: process.env.SOURCE_S3_REGION ?? "default",
|
||||||
@@ -24,13 +45,21 @@ const SOURCE_S3 = {
|
|||||||
bucket: process.env.SOURCE_S3_BUCKET ?? "test",
|
bucket: process.env.SOURCE_S3_BUCKET ?? "test",
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEST_S3 = {
|
const destEndpoint = normalizeParsPackEndpoint(
|
||||||
endpoint:
|
|
||||||
process.env.DEST_S3_ENDPOINT ??
|
process.env.DEST_S3_ENDPOINT ??
|
||||||
process.env.SOURCE_S3_ENDPOINT ??
|
process.env.SOURCE_S3_ENDPOINT ??
|
||||||
"http://95.156.253.15:9001/",
|
"http://95.156.253.15:9001/",
|
||||||
|
);
|
||||||
|
const destBucket = resolveParsPackBucket(
|
||||||
|
destEndpoint,
|
||||||
|
process.env.DEST_S3_BUCKET ?? "migrations",
|
||||||
|
);
|
||||||
|
|
||||||
|
const DEST_S3 = {
|
||||||
|
endpoint: destEndpoint,
|
||||||
region:
|
region:
|
||||||
process.env.DEST_S3_REGION ?? process.env.SOURCE_S3_REGION ?? "default",
|
process.env.DEST_S3_REGION ??
|
||||||
|
(destEndpoint.includes("parspack.net") ? "us-west-2" : process.env.SOURCE_S3_REGION ?? "default"),
|
||||||
accessKeyId:
|
accessKeyId:
|
||||||
process.env.DEST_S3_ACCESS_KEY ??
|
process.env.DEST_S3_ACCESS_KEY ??
|
||||||
process.env.SOURCE_S3_ACCESS_KEY ??
|
process.env.SOURCE_S3_ACCESS_KEY ??
|
||||||
@@ -39,11 +68,19 @@ const DEST_S3 = {
|
|||||||
process.env.DEST_S3_SECRET_KEY ??
|
process.env.DEST_S3_SECRET_KEY ??
|
||||||
process.env.SOURCE_S3_SECRET_KEY ??
|
process.env.SOURCE_S3_SECRET_KEY ??
|
||||||
"qbyQPjiCd5RcfIFpA3l2sG7EW9nT1gtVYkKJN08w",
|
"qbyQPjiCd5RcfIFpA3l2sG7EW9nT1gtVYkKJN08w",
|
||||||
bucket: process.env.DEST_S3_BUCKET ?? "migrations",
|
bucket: destBucket,
|
||||||
};
|
};
|
||||||
|
|
||||||
function createS3Client(config) {
|
function createS3Client(config, label) {
|
||||||
return new S3Client({
|
const step = `createS3Client:${label}`;
|
||||||
|
logger.info(step, "creating S3 client", {
|
||||||
|
endpoint: config.endpoint,
|
||||||
|
region: config.region,
|
||||||
|
bucket: config.bucket,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new S3Client({
|
||||||
region: config.region,
|
region: config.region,
|
||||||
endpoint: config.endpoint,
|
endpoint: config.endpoint,
|
||||||
forcePathStyle: true,
|
forcePathStyle: true,
|
||||||
@@ -52,39 +89,70 @@ function createS3Client(config) {
|
|||||||
secretAccessKey: config.secretAccessKey,
|
secretAccessKey: config.secretAccessKey,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
logger.info(step, "S3 client created");
|
||||||
|
return client;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(step, "failed to create S3 client", logger.formatError(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAssetById(id) {
|
async function getAssetById(id) {
|
||||||
|
const step = `getAssetById:${id}`;
|
||||||
|
try {
|
||||||
const assetRepository = AppDataSource.getRepository(AssetEntity);
|
const assetRepository = AppDataSource.getRepository(AssetEntity);
|
||||||
return assetRepository.findOneBy({ id });
|
const asset = await assetRepository.findOneBy({ id });
|
||||||
|
logger.debug(step, asset ? "asset found" : "asset not found");
|
||||||
|
return asset;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(step, "database query failed", logger.formatError(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBrandAssetsByGroupId(groupId) {
|
async function getBrandAssetsByGroupId(groupId) {
|
||||||
|
const step = `getBrandAssetsByGroupId:${groupId}`;
|
||||||
|
try {
|
||||||
const brandAssetRepo = AppDataSource.getRepository(BrandAssetEntity);
|
const brandAssetRepo = AppDataSource.getRepository(BrandAssetEntity);
|
||||||
return brandAssetRepo.find({
|
const brandAssets = await brandAssetRepo.find({
|
||||||
where: {
|
where: {
|
||||||
groupId,
|
groupId,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
logger.debug(step, `found ${brandAssets.length} brand assets`);
|
||||||
|
return brandAssets;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(step, "database query failed", logger.formatError(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBrandUrlNames() {
|
async function getBrandUrlNames() {
|
||||||
|
const step = "getBrandUrlNames";
|
||||||
|
try {
|
||||||
const brandRepo = AppDataSource.getRepository(BrandEntity);
|
const brandRepo = AppDataSource.getRepository(BrandEntity);
|
||||||
const brands = await brandRepo.find({
|
const brands = await brandRepo.find({
|
||||||
where: {
|
where: {
|
||||||
deleted: false,
|
deleted: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
logger.info(step, `loaded ${brands.length} brands from database`);
|
||||||
|
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
for (const brand of brands) {
|
for (const brand of brands) {
|
||||||
if (!brand.urlName) continue;
|
if (!brand.urlName) {
|
||||||
|
logger.debug(step, `skipping brand id=${brand.id}: missing urlName`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const key = `${brand.groupId}:${brand.urlName.toLowerCase()}`;
|
const key = `${brand.groupId}:${brand.urlName.toLowerCase()}`;
|
||||||
if (seen.has(key)) continue;
|
if (seen.has(key)) {
|
||||||
|
logger.debug(step, `skipping duplicate brand urlName=${brand.urlName}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
result.push({
|
result.push({
|
||||||
@@ -93,7 +161,12 @@ async function getBrandUrlNames() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(step, `resolved ${result.length} unique brands`);
|
||||||
return result;
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(step, "failed to load brands", logger.formatError(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAssetSourceKey(asset) {
|
function resolveAssetSourceKey(asset) {
|
||||||
@@ -101,8 +174,10 @@ function resolveAssetSourceKey(asset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function objectExists(client, bucket, key) {
|
async function objectExists(client, bucket, key) {
|
||||||
|
const step = `objectExists:${bucket}/${key}`;
|
||||||
try {
|
try {
|
||||||
await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
||||||
|
logger.debug(step, "object exists");
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const statusCode =
|
const statusCode =
|
||||||
@@ -115,7 +190,12 @@ async function objectExists(client, bucket, key) {
|
|||||||
? error.$metadata.httpStatusCode
|
? error.$metadata.httpStatusCode
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (statusCode === 404) return false;
|
if (statusCode === 404) {
|
||||||
|
logger.debug(step, "object not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(step, "head object failed", logger.formatError(error));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,6 +208,9 @@ async function copyFileCrossClient(
|
|||||||
sourceKey,
|
sourceKey,
|
||||||
destKey,
|
destKey,
|
||||||
) {
|
) {
|
||||||
|
const step = `copyFileCrossClient:${sourceBucket}/${sourceKey}->${destBucket}/${destKey}`;
|
||||||
|
try {
|
||||||
|
logger.debug(step, "fetching source object");
|
||||||
const response = await sourceClient.send(
|
const response = await sourceClient.send(
|
||||||
new GetObjectCommand({
|
new GetObjectCommand({
|
||||||
Bucket: sourceBucket,
|
Bucket: sourceBucket,
|
||||||
@@ -139,6 +222,11 @@ async function copyFileCrossClient(
|
|||||||
throw new Error(`Empty response body for ${sourceBucket}/${sourceKey}`);
|
throw new Error(`Empty response body for ${sourceBucket}/${sourceKey}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug(step, "uploading to destination", {
|
||||||
|
contentType: response.ContentType,
|
||||||
|
contentLength: response.ContentLength,
|
||||||
|
});
|
||||||
|
|
||||||
await destClient.send(
|
await destClient.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: destBucket,
|
Bucket: destBucket,
|
||||||
@@ -151,6 +239,12 @@ async function copyFileCrossClient(
|
|||||||
...(response.Metadata ? { Metadata: response.Metadata } : {}),
|
...(response.Metadata ? { Metadata: response.Metadata } : {}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
logger.info(step, "copy completed");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(step, "copy failed", logger.formatError(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyIfNeeded(
|
async function copyIfNeeded(
|
||||||
@@ -163,22 +257,34 @@ async function copyIfNeeded(
|
|||||||
dryRun,
|
dryRun,
|
||||||
type = "file",
|
type = "file",
|
||||||
) {
|
) {
|
||||||
|
const step = `copyIfNeeded:${type}:${destBucket}/${destKey}`;
|
||||||
|
try {
|
||||||
if (type !== "directory") {
|
if (type !== "directory") {
|
||||||
const sourceExists = await objectExists(
|
const sourceExists = await objectExists(
|
||||||
sourceClient,
|
sourceClient,
|
||||||
sourceBucket,
|
sourceBucket,
|
||||||
sourceKey,
|
sourceKey,
|
||||||
);
|
);
|
||||||
if (!sourceExists) return "missing";
|
if (!sourceExists) {
|
||||||
|
logger.warn(step, "source object missing", {
|
||||||
|
sourceBucket,
|
||||||
|
sourceKey,
|
||||||
|
});
|
||||||
|
return "missing";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const destExists = await objectExists(destClient, destBucket, destKey);
|
const destExists = await objectExists(destClient, destBucket, destKey);
|
||||||
if (destExists) return "skipped";
|
if (destExists) {
|
||||||
|
logger.debug(step, "destination already exists, skipping");
|
||||||
|
return "skipped";
|
||||||
|
}
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
console.log(
|
logger.info(step, "[dry-run] would copy object", {
|
||||||
`[dry-run] copy ${sourceBucket}/${sourceKey} -> ${destBucket}/${destKey}`,
|
source: `${sourceBucket}/${sourceKey}`,
|
||||||
);
|
destination: `${destBucket}/${destKey}`,
|
||||||
|
});
|
||||||
return "copied";
|
return "copied";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +298,7 @@ async function copyIfNeeded(
|
|||||||
destKey,
|
destKey,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
logger.debug(step, "creating directory marker");
|
||||||
await destClient.send(
|
await destClient.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: destBucket,
|
Bucket: destBucket,
|
||||||
@@ -200,12 +307,24 @@ async function copyIfNeeded(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(step, "copy action completed", { result: "copied" });
|
||||||
return "copied";
|
return "copied";
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(step, "copyIfNeeded failed", logger.formatError(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createBrandsFolders(sourceClient, destClient, brands, stats) {
|
async function createBrandsFolders(sourceClient, destClient, brands, stats) {
|
||||||
|
const step = "createBrandsFolders";
|
||||||
|
logger.info(step, `creating folders for ${brands.length} brands`);
|
||||||
|
|
||||||
for (const brand of brands) {
|
for (const brand of brands) {
|
||||||
|
const brandStep = `${step}:${brand.name}`;
|
||||||
|
try {
|
||||||
const folderKey = `${brand.name.toLowerCase()}/`;
|
const folderKey = `${brand.name.toLowerCase()}/`;
|
||||||
|
logger.debug(brandStep, "creating folder", { folderKey });
|
||||||
|
|
||||||
const result = await copyIfNeeded(
|
const result = await copyIfNeeded(
|
||||||
sourceClient,
|
sourceClient,
|
||||||
destClient,
|
destClient,
|
||||||
@@ -219,6 +338,12 @@ async function createBrandsFolders(sourceClient, destClient, brands, stats) {
|
|||||||
|
|
||||||
if (result === "copied") stats.foldersCreated++;
|
if (result === "copied") stats.foldersCreated++;
|
||||||
if (result === "skipped") stats.foldersSkipped++;
|
if (result === "skipped") stats.foldersSkipped++;
|
||||||
|
|
||||||
|
logger.info(brandStep, "folder step completed", { result });
|
||||||
|
} catch (error) {
|
||||||
|
stats.errors++;
|
||||||
|
logger.error(brandStep, "folder creation failed", logger.formatError(error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +354,8 @@ async function copyAssetVariant(
|
|||||||
destKey,
|
destKey,
|
||||||
stats,
|
stats,
|
||||||
) {
|
) {
|
||||||
|
const step = `copyAssetVariant:${destKey}`;
|
||||||
|
try {
|
||||||
const result = await copyIfNeeded(
|
const result = await copyIfNeeded(
|
||||||
sourceClient,
|
sourceClient,
|
||||||
destClient,
|
destClient,
|
||||||
@@ -243,23 +370,38 @@ async function copyAssetVariant(
|
|||||||
if (result === "copied") stats.assetsCopied++;
|
if (result === "copied") stats.assetsCopied++;
|
||||||
if (result === "skipped") stats.assetsSkipped++;
|
if (result === "skipped") stats.assetsSkipped++;
|
||||||
if (result === "missing") stats.assetsMissing++;
|
if (result === "missing") stats.assetsMissing++;
|
||||||
|
|
||||||
|
logger.debug(step, "variant copy completed", { result });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(step, "variant copy failed", logger.formatError(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyBrandAssets(sourceClient, destClient, brands, stats) {
|
async function copyBrandAssets(sourceClient, destClient, brands, stats) {
|
||||||
|
const step = "copyBrandAssets";
|
||||||
|
logger.info(step, `copying assets for ${brands.length} brands`);
|
||||||
|
|
||||||
for (const brand of brands) {
|
for (const brand of brands) {
|
||||||
|
const brandStep = `${step}:${brand.name}`;
|
||||||
|
try {
|
||||||
const brandAssets = await getBrandAssetsByGroupId(brand.groupId);
|
const brandAssets = await getBrandAssetsByGroupId(brand.groupId);
|
||||||
const brandFolder = brand.name.toLowerCase();
|
const brandFolder = brand.name.toLowerCase();
|
||||||
|
|
||||||
console.log(
|
logger.info(brandStep, "processing brand assets", {
|
||||||
`Processing brand "${brand.name}" (groupId=${brand.groupId}, assets=${brandAssets.length})`,
|
groupId: brand.groupId,
|
||||||
);
|
assetCount: brandAssets.length,
|
||||||
|
});
|
||||||
|
|
||||||
for (const brandAsset of brandAssets) {
|
for (const brandAsset of brandAssets) {
|
||||||
|
const assetStep = `${brandStep}:asset:${brandAsset.assetId}`;
|
||||||
|
try {
|
||||||
const asset = await getAssetById(brandAsset.assetId);
|
const asset = await getAssetById(brandAsset.assetId);
|
||||||
if (!asset || asset.deleted) {
|
if (!asset || asset.deleted) {
|
||||||
console.warn(
|
logger.warn(assetStep, "asset not found or deleted", {
|
||||||
`Skipping brand_asset ${brandAsset.id}: asset ${brandAsset.assetId} not found or deleted`,
|
brandAssetId: brandAsset.id,
|
||||||
);
|
assetId: brandAsset.assetId,
|
||||||
|
});
|
||||||
stats.assetsMissing++;
|
stats.assetsMissing++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -267,7 +409,6 @@ async function copyBrandAssets(sourceClient, destClient, brands, stats) {
|
|||||||
const sourceKey = resolveAssetSourceKey(asset);
|
const sourceKey = resolveAssetSourceKey(asset);
|
||||||
const destBase = `${brandFolder}/${asset.id}`;
|
const destBase = `${brandFolder}/${asset.id}`;
|
||||||
|
|
||||||
try {
|
|
||||||
await copyAssetVariant(
|
await copyAssetVariant(
|
||||||
sourceClient,
|
sourceClient,
|
||||||
destClient,
|
destClient,
|
||||||
@@ -291,29 +432,66 @@ async function copyBrandAssets(sourceClient, destClient, brands, stats) {
|
|||||||
// );
|
// );
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
stats.errors++;
|
stats.errors++;
|
||||||
console.error(
|
logger.error(assetStep, "asset copy failed", logger.formatError(error));
|
||||||
`Failed to copy asset ${asset.id} for brand "${brand.name}":`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(brandStep, "brand assets completed");
|
||||||
|
} catch (error) {
|
||||||
|
stats.errors++;
|
||||||
|
logger.error(brandStep, "brand processing failed", logger.formatError(error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function printSummary(stats) {
|
function printSummary(stats) {
|
||||||
console.log("\nMigration summary:");
|
const step = "printSummary";
|
||||||
console.log(` Folders created: ${stats.foldersCreated}`);
|
logger.info(step, "migration summary", {
|
||||||
console.log(` Folders skipped: ${stats.foldersSkipped}`);
|
foldersCreated: stats.foldersCreated,
|
||||||
console.log(` Assets copied: ${stats.assetsCopied}`);
|
foldersSkipped: stats.foldersSkipped,
|
||||||
console.log(` Assets skipped: ${stats.assetsSkipped}`);
|
assetsCopied: stats.assetsCopied,
|
||||||
console.log(` Assets missing: ${stats.assetsMissing}`);
|
assetsSkipped: stats.assetsSkipped,
|
||||||
console.log(` Errors: ${stats.errors}`);
|
assetsMissing: stats.assetsMissing,
|
||||||
if (DRY_RUN) console.log(" (dry-run mode — no objects were written)");
|
errors: stats.errors,
|
||||||
|
dryRun: DRY_RUN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeDataSource() {
|
||||||
|
const step = "initializeDataSource";
|
||||||
|
try {
|
||||||
|
await AppDataSource.initialize();
|
||||||
|
logger.info(step, "data source initialized", {
|
||||||
|
host: AppDataSource.options.host,
|
||||||
|
port: AppDataSource.options.port,
|
||||||
|
database: AppDataSource.options.database,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(step, "data source initialization failed", logger.formatError(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function destroyDataSource() {
|
||||||
|
const step = "destroyDataSource";
|
||||||
|
if (!AppDataSource.isInitialized) {
|
||||||
|
logger.debug(step, "data source not initialized, skipping destroy");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AppDataSource.destroy();
|
||||||
|
logger.info(step, "data source destroyed");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(step, "data source destroy failed", logger.formatError(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const sourceClient = createS3Client(SOURCE_S3);
|
const step = "main";
|
||||||
const destClient = createS3Client(DEST_S3);
|
logger.info(step, "migration started", { dryRun: DRY_RUN });
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
foldersCreated: 0,
|
foldersCreated: 0,
|
||||||
foldersSkipped: 0,
|
foldersSkipped: 0,
|
||||||
@@ -324,30 +502,52 @@ async function main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await AppDataSource.initialize();
|
const sourceClient = await runStep("createSourceS3Client", async () =>
|
||||||
console.log("Data source initialized");
|
createS3Client(SOURCE_S3, "source"),
|
||||||
console.log(`Source S3: ${SOURCE_S3.endpoint} (${SOURCE_S3.bucket})`);
|
);
|
||||||
console.log(`Dest S3: ${DEST_S3.endpoint} (${DEST_S3.bucket})`);
|
const destClient = await runStep("createDestS3Client", async () =>
|
||||||
|
createS3Client(DEST_S3, "dest"),
|
||||||
|
);
|
||||||
|
|
||||||
const brands = await getBrandUrlNames();
|
logger.info(step, "S3 configuration", {
|
||||||
console.log(`Found ${brands.length} brands to migrate`);
|
source: { endpoint: SOURCE_S3.endpoint, bucket: SOURCE_S3.bucket },
|
||||||
|
dest: { endpoint: DEST_S3.endpoint, bucket: DEST_S3.bucket },
|
||||||
|
});
|
||||||
|
|
||||||
await createBrandsFolders(sourceClient, destClient, brands, stats);
|
await runStep("initializeDataSource", initializeDataSource);
|
||||||
await copyBrandAssets(sourceClient, destClient, brands, stats);
|
|
||||||
|
const brands = await runStep("getBrandUrlNames", getBrandUrlNames);
|
||||||
|
|
||||||
|
await runStep("createBrandsFolders", () =>
|
||||||
|
createBrandsFolders(sourceClient, destClient, brands, stats),
|
||||||
|
);
|
||||||
|
|
||||||
|
await runStep("copyBrandAssets", () =>
|
||||||
|
copyBrandAssets(sourceClient, destClient, brands, stats),
|
||||||
|
);
|
||||||
|
|
||||||
printSummary(stats);
|
printSummary(stats);
|
||||||
|
|
||||||
if (stats.errors > 0) {
|
if (stats.errors > 0) {
|
||||||
|
logger.warn(step, `migration finished with ${stats.errors} error(s)`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
} else {
|
||||||
|
logger.info(step, "migration completed successfully");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
console.error("Migration failed:", err);
|
logger.error(step, "migration failed", logger.formatError(error));
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
} finally {
|
} finally {
|
||||||
if (AppDataSource.isInitialized) {
|
try {
|
||||||
await AppDataSource.destroy();
|
await destroyDataSource();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(step, "cleanup failed", logger.formatError(error));
|
||||||
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main().catch((error) => {
|
||||||
|
logger.error("unhandledRejection", "fatal error in main()", logger.formatError(error));
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
|||||||
65
src/logger.js
Normal file
65
src/logger.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
function timestamp() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatError(error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
...(typeof error === "object" && error !== null && "$metadata" in error
|
||||||
|
? { metadata: error.$metadata }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value: String(error) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const consoleMethods = {
|
||||||
|
info: console.log,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error,
|
||||||
|
debug: console.log,
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(level, step, message, details) {
|
||||||
|
const prefix = `[${timestamp()}] [${level}] [${step}]`;
|
||||||
|
const write = consoleMethods[level] ?? console.log;
|
||||||
|
|
||||||
|
if (details !== undefined) {
|
||||||
|
write(`${prefix} ${message}`, details);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
write(`${prefix} ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = {
|
||||||
|
info(step, message, details) {
|
||||||
|
log("info", step, message, details);
|
||||||
|
},
|
||||||
|
warn(step, message, details) {
|
||||||
|
log("warn", step, message, details);
|
||||||
|
},
|
||||||
|
error(step, message, details) {
|
||||||
|
log("error", step, message, details);
|
||||||
|
},
|
||||||
|
debug(step, message, details) {
|
||||||
|
log("debug", step, message, details);
|
||||||
|
},
|
||||||
|
formatError,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function runStep(step, fn) {
|
||||||
|
logger.info(step, "starting");
|
||||||
|
try {
|
||||||
|
const result = await fn();
|
||||||
|
logger.info(step, "completed");
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(step, "failed", formatError(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user