refactor: adding logger and catch blocks

This commit is contained in:
2026-06-12 20:19:25 +03:30
parent 2565da621f
commit 6111016245
4 changed files with 484 additions and 208 deletions

View File

@@ -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"

View File

@@ -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);

View File

@@ -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
View 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;
}
}