chore: init commit

This commit is contained in:
2026-06-08 14:06:13 +03:30
commit debdb70a52
9 changed files with 3084 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "migrations",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"start:dry-run": "DRY_RUN=true node src/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "^11.5.1",
"onFail": "download"
}
},
"type": "module",
"dependencies": {
"@aws-sdk/client-s3": "3.645.0",
"@aws-sdk/s3-request-presigner": "^3.940.0",
"mysql2": "^3.14.4",
"reflect-metadata": "^0.2.2",
"typeorm": "0.3.20"
}
}

2523
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

18
src/data-source.js Normal file
View File

@@ -0,0 +1,18 @@
import "reflect-metadata";
import { DataSource } from "typeorm";
import { AssetEntity } from "./entity/Assets.js";
import { BrandEntity } from "./entity/Brand.js";
import { BrandAssetEntity } from "./entity/BrandAsset.js";
export const AppDataSource = new DataSource({
type: "mysql",
host: "95.156.253.15",
port: 3306,
username: "root",
password: "Aa9923994970",
database: "temporary",
logging: false,
entities: [AssetEntity, BrandEntity, BrandAssetEntity],
migrations: [],
subscribers: [],
});

51
src/entity/Assets.js Normal file
View File

@@ -0,0 +1,51 @@
import { EntitySchema } from "typeorm";
export const AssetEntity = new EntitySchema({
name: "AssetEntity",
tableName: "assets",
columns: {
id: {
type: Number,
primary: true,
generated: "increment",
},
name: {
type: String,
},
type: {
type: String,
},
size: {
type: Number,
},
description: {
type: "text",
},
path: {
type: String,
},
isVisible: {
type: Boolean,
default: false,
},
parentId: {
type: Number,
nullable: true,
},
createdBy: {
type: String,
},
createdAt: {
type: Date,
},
lastModifiedBy: {
type: String,
},
lastModifiedAt: {
type: Date,
},
deleted: {
type: Boolean,
},
},
});

47
src/entity/Brand.js Normal file
View File

@@ -0,0 +1,47 @@
import { EntitySchema } from "typeorm";
export const BrandEntity = new EntitySchema({
name: "BrandEntity",
tableName: "brand",
columns: {
id: {
type: Number,
primary: true,
generated: true,
},
groupId: {
type: String,
},
createdBy: {
type: String,
},
createdAt: {
type: Date,
},
lastModifiedBy: {
type: String,
},
lastModifiedAt: {
type: Date,
},
registerCode: {
type: String,
nullable: true,
},
typeId: {
type: Number,
nullable: true,
},
logoAssetId: {
type: String,
nullable: true,
},
urlName: {
type: String,
nullable: true,
},
deleted: {
type: Boolean,
},
},
});

43
src/entity/BrandAsset.js Normal file
View File

@@ -0,0 +1,43 @@
import { EntitySchema } from "typeorm";
export const BrandAssetEntity = new EntitySchema({
name: "BrandAssetEntity",
tableName: "brand_asset",
columns: {
id: {
type: Number,
primary: true,
generated: true,
},
groupId: {
type: String,
},
assetId: {
type: Number,
},
hash: {
type: String,
},
createdBy: {
type: String,
},
createdAt: {
type: Date,
},
lastModifiedBy: {
type: String,
},
lastModifiedAt: {
type: Date,
},
color: {
type: String,
},
general: {
type: Boolean,
},
deleted: {
type: Boolean,
},
},
});

21
src/entity/User.js Normal file
View File

@@ -0,0 +1,21 @@
import { EntitySchema } from "typeorm";
export const User = new EntitySchema({
name: "User",
columns: {
id: {
type: Number,
primary: true,
generated: true,
},
firstName: {
type: String,
},
lastName: {
type: String,
},
age: {
type: Number,
},
},
});

352
src/index.js Normal file
View File

@@ -0,0 +1,352 @@
import {
GetObjectCommand,
HeadObjectCommand,
PutObjectCommand,
S3Client,
} from "@aws-sdk/client-s3";
import { AppDataSource } from "./data-source.js";
import { AssetEntity } from "./entity/Assets.js";
import { BrandEntity } from "./entity/Brand.js";
import { BrandAssetEntity } from "./entity/BrandAsset.js";
const AVATAR_PREFIX = "avatar";
const THUMBNAIL_PREFIX = "lg";
const DRY_RUN = process.env.DRY_RUN === "true";
const SOURCE_S3 = {
endpoint: process.env.SOURCE_S3_ENDPOINT ?? "http://95.156.253.15:9001/",
region: process.env.SOURCE_S3_REGION ?? "default",
accessKeyId: process.env.SOURCE_S3_ACCESS_KEY ?? "sirCxqNKw85GdnlLfzko",
secretAccessKey:
process.env.SOURCE_S3_SECRET_KEY ??
"qbyQPjiCd5RcfIFpA3l2sG7EW9nT1gtVYkKJN08w",
bucket: process.env.SOURCE_S3_BUCKET ?? "test",
};
const DEST_S3 = {
endpoint:
process.env.DEST_S3_ENDPOINT ??
process.env.SOURCE_S3_ENDPOINT ??
"http://95.156.253.15:9001/",
region:
process.env.DEST_S3_REGION ?? process.env.SOURCE_S3_REGION ?? "default",
accessKeyId:
process.env.DEST_S3_ACCESS_KEY ??
process.env.SOURCE_S3_ACCESS_KEY ??
"sirCxqNKw85GdnlLfzko",
secretAccessKey:
process.env.DEST_S3_SECRET_KEY ??
process.env.SOURCE_S3_SECRET_KEY ??
"qbyQPjiCd5RcfIFpA3l2sG7EW9nT1gtVYkKJN08w",
bucket: process.env.DEST_S3_BUCKET ?? "migrations",
};
function createS3Client(config) {
return new S3Client({
region: config.region,
endpoint: config.endpoint,
forcePathStyle: true,
credentials: {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
});
}
async function getAssetById(id) {
const assetRepository = AppDataSource.getRepository(AssetEntity);
return assetRepository.findOneBy({ id });
}
async function getBrandAssetsByGroupId(groupId) {
const brandAssetRepo = AppDataSource.getRepository(BrandAssetEntity);
return brandAssetRepo.find({
where: {
groupId,
deleted: false,
},
});
}
async function getBrandUrlNames() {
const brandRepo = AppDataSource.getRepository(BrandEntity);
const brands = await brandRepo.find({
where: {
deleted: false,
},
});
const seen = new Set();
const result = [];
for (const brand of brands) {
if (!brand.urlName) continue;
const key = `${brand.groupId}:${brand.urlName.toLowerCase()}`;
if (seen.has(key)) continue;
seen.add(key);
result.push({
name: brand.urlName,
groupId: brand.groupId,
});
}
return result;
}
function resolveAssetSourceKey(asset) {
return asset.id.toString();
}
async function objectExists(client, bucket, key) {
try {
await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
return true;
} catch (error) {
const statusCode =
typeof error === "object" &&
error !== null &&
"$metadata" in error &&
typeof error.$metadata === "object" &&
error.$metadata !== null &&
"httpStatusCode" in error.$metadata
? error.$metadata.httpStatusCode
: undefined;
if (statusCode === 404) return false;
throw error;
}
}
async function copyFileCrossClient(
sourceClient,
destClient,
sourceBucket,
destBucket,
sourceKey,
destKey,
) {
const response = await sourceClient.send(
new GetObjectCommand({
Bucket: sourceBucket,
Key: sourceKey,
}),
);
if (!response.Body) {
throw new Error(`Empty response body for ${sourceBucket}/${sourceKey}`);
}
await destClient.send(
new PutObjectCommand({
Bucket: destBucket,
Key: destKey,
Body: response.Body,
...(response.ContentType ? { ContentType: response.ContentType } : {}),
...(response.ContentLength !== undefined
? { ContentLength: response.ContentLength }
: {}),
...(response.Metadata ? { Metadata: response.Metadata } : {}),
}),
);
}
async function copyIfNeeded(
sourceClient,
destClient,
sourceBucket,
destBucket,
sourceKey,
destKey,
dryRun,
type = "file",
) {
if (type !== "directory") {
const sourceExists = await objectExists(
sourceClient,
sourceBucket,
sourceKey,
);
if (!sourceExists) return "missing";
}
const destExists = await objectExists(destClient, destBucket, destKey);
if (destExists) return "skipped";
if (dryRun) {
console.log(
`[dry-run] copy ${sourceBucket}/${sourceKey} -> ${destBucket}/${destKey}`,
);
return "copied";
}
if (type === "file") {
await copyFileCrossClient(
sourceClient,
destClient,
sourceBucket,
destBucket,
sourceKey,
destKey,
);
} else {
await destClient.send(
new PutObjectCommand({
Bucket: destBucket,
Key: destKey,
}),
);
}
return "copied";
}
async function createBrandsFolders(sourceClient, destClient, brands, stats) {
for (const brand of brands) {
const folderKey = `${brand.name.toLowerCase()}/`;
const result = await copyIfNeeded(
sourceClient,
destClient,
SOURCE_S3.bucket,
DEST_S3.bucket,
"",
folderKey,
DRY_RUN,
"directory",
);
if (result === "copied") stats.foldersCreated++;
if (result === "skipped") stats.foldersSkipped++;
}
}
async function copyAssetVariant(
sourceClient,
destClient,
sourceKey,
destKey,
stats,
) {
const result = await copyIfNeeded(
sourceClient,
destClient,
SOURCE_S3.bucket,
DEST_S3.bucket,
sourceKey,
destKey,
DRY_RUN,
"file",
);
if (result === "copied") stats.assetsCopied++;
if (result === "skipped") stats.assetsSkipped++;
if (result === "missing") stats.assetsMissing++;
}
async function copyBrandAssets(sourceClient, destClient, brands, stats) {
for (const brand of brands) {
const brandAssets = await getBrandAssetsByGroupId(brand.groupId);
const brandFolder = brand.name.toLowerCase();
console.log(
`Processing brand "${brand.name}" (groupId=${brand.groupId}, assets=${brandAssets.length})`,
);
for (const brandAsset of brandAssets) {
const asset = await getAssetById(brandAsset.assetId);
if (!asset || asset.deleted) {
console.warn(
`Skipping brand_asset ${brandAsset.id}: asset ${brandAsset.assetId} not found or deleted`,
);
stats.assetsMissing++;
continue;
}
const sourceKey = resolveAssetSourceKey(asset);
const destBase = `${brandFolder}/${asset.id}`;
try {
await copyAssetVariant(
sourceClient,
destClient,
sourceKey,
destBase,
stats,
);
// await copyAssetVariant(
// sourceClient,
// destClient,
// `${AVATAR_PREFIX}/${sourceKey}`,
// `${destBase}/${AVATAR_PREFIX}`,
// stats,
// );
// await copyAssetVariant(
// sourceClient,
// destClient,
// `${THUMBNAIL_PREFIX}/${sourceKey}`,
// `${destBase}/${THUMBNAIL_PREFIX}`,
// stats,
// );
} catch (error) {
stats.errors++;
console.error(
`Failed to copy asset ${asset.id} for brand "${brand.name}":`,
error,
);
}
}
}
}
function printSummary(stats) {
console.log("\nMigration summary:");
console.log(` Folders created: ${stats.foldersCreated}`);
console.log(` Folders skipped: ${stats.foldersSkipped}`);
console.log(` Assets copied: ${stats.assetsCopied}`);
console.log(` Assets skipped: ${stats.assetsSkipped}`);
console.log(` Assets missing: ${stats.assetsMissing}`);
console.log(` Errors: ${stats.errors}`);
if (DRY_RUN) console.log(" (dry-run mode — no objects were written)");
}
async function main() {
const sourceClient = createS3Client(SOURCE_S3);
const destClient = createS3Client(DEST_S3);
const stats = {
foldersCreated: 0,
foldersSkipped: 0,
assetsCopied: 0,
assetsSkipped: 0,
assetsMissing: 0,
errors: 0,
};
try {
await AppDataSource.initialize();
console.log("Data source initialized");
console.log(`Source S3: ${SOURCE_S3.endpoint} (${SOURCE_S3.bucket})`);
console.log(`Dest S3: ${DEST_S3.endpoint} (${DEST_S3.bucket})`);
const brands = await getBrandUrlNames();
console.log(`Found ${brands.length} brands to migrate`);
await createBrandsFolders(sourceClient, destClient, brands, stats);
await copyBrandAssets(sourceClient, destClient, brands, stats);
printSummary(stats);
if (stats.errors > 0) {
process.exitCode = 1;
}
} catch (err) {
console.error("Migration failed:", err);
process.exitCode = 1;
} finally {
if (AppDataSource.isInitialized) {
await AppDataSource.destroy();
}
}
}
main();