guide从自定义来源迁移

要从自定义来源迁移文件,您需要创建自己的 SourceStorageAdapter 实现。适配器准备迁移计划,其中包含要迁移的类别、文件夹和资产列表。它们还公开了一种方法,允许迁移程序从存储中下载文件。

要创建一个新的适配器,必须首先在 adapters/ 目录中创建一个目录。

下一步是创建 Adapter.ts 文件。这是适配器的入口点,您无法更改此文件名。要创建文件,您可以使用以下样板

import {
    ISourceStorageAdapter,
    IMigrationPlan,
    ISourceCategory,
    ISourceFolder,
    ISourceAsset
} from '@ckbox-migrator';

export default class ExampleAdapter implements ISourceStorageAdapter {
    public readonly name: string = 'The name of your system';

    public async loadConfig( plainConfig: Record<string, unknown> ): Promise<void> {
        // Load and validate configuration.
    }

    public async verifyConnection(): Promise<void> {
        // Verify if you can connect and authorize to your source storage.
    }

    public async prepareMigrationPlan(): Promise<IMigrationPlan> {
        // Scan your source storage and identify the categories,
        // folders and assets to migrate.

        return [
            categories: [],
            assets: []
        ];
    }

    public async getAsset( downloadUrl: string ): Promise<IGetAssetResult> {
        // Get an asset content from you source storage.
    }
}

# 加载配置

要加载配置,您需要实现 loadConfig( plainConfig: Record<string, unknown> ) 方法。这是迁移过程中执行的第一步。提供给该方法的参数是 config.json 文件中的 source.options 属性。

为了验证配置,我们建议使用 class-validatorclass-transformer 库。它们已与迁移程序捆绑在一起,因此您无需单独安装它们。

首先,您需要创建配置类。它将包含配置属性和验证规则。

class ExampleConfig {
    @IsString()
    @IsDefined()
    public readonly foo: string;
}

然后,您可以使用 plainToInstance 映射配置,并使用 validateOrReject 方法验证它。

export default class ExampleAdapter implements ISourceStorageAdapter {

    // ...

    private _config: ExampleConfig;

    public async loadConfig( plainConfig: Record<string, unknown> ): Promise<void> {
        this._config = plainToInstance( CKFinderConfig, plainConfig );

        await validateOrReject( this._config );
    }

现在,您可以配置适配器。您需要将 source.type 设置为创建适配器的文件夹的名称,并将与 ExampleConfig 兼容的配置设置为 source.options 中。

{
    "source": {
        "type": "example",
        "options": {
            "foo": "bar"
        }
    },
    "ckbox": {
        ...
    }
}

要构建应用程序,请使用以下命令

npm run build:adapters

# 验证连接

验证连接是用于检查迁移程序是否可以建立与源存储的连接的一步。您可以向源系统中的任何端点发出请求以检查授权是否通过。

import fetch, { Response } from 'node-fetch';

export default class ExampleAdapter implements ISourceStorageAdapter {

        // ...

        const { serviceOrigin } = this._config;

        const response: Response = await fetch( `${ serviceOrigin }/status` );

        if ( !response.ok ) {
            throw new Error( `Failed to connect to the Example service at ${ serviceOrigin }.` );
        }

# 创建迁移计划

在迁移开始之前,适配器应返回迁移计划,该结构包含要创建的类别、文件夹和资产列表。

您应该从源存储请求所有资源的列表,并映射到以下格式。

export interface IMigrationPlan {
    readonly categories: ISourceCategory[];
    readonly assets: ISourceAsset[];
}

迁移计划应由迁移程序的 prepareMigrationPlan 返回

import crypto from 'node:crypto';

export default class ExampleAdapter implements ISourceStorageAdapter {

    // ...

    public async prepareMigrationPlan(): Promise<IMigrationPlan> {
        const categoryId: string = crypto.randomUUID();

        return [
            categories: [{
                id: categoryId,
                name: 'Example category',
                allowedExtensions: ['txt'],
                folders: []
            }],
            assets: [{
                id: crypto.randomUUID(),
                name: 'File',
                extension: 'txt',
                location: { categoryId }
                downloadUrl: 'https://127.0.0.1/file.txt'
                downloadUrlToReplace: 'https://127.0.0.1/file.txt'
            }]
        ];
    }

# 类别和文件夹

每个类别都应具有唯一的标识符(它无关紧要,它是源系统生成的还是在适配器中使用随机 ID 生成器生成的),名称,允许的扩展列表(请记住,它至少应涵盖将上传到该类别的扩展名)和文件夹树。

export interface ISourceCategory {
    readonly id: string;
    readonly name: string;
    readonly allowedExtensions: string[];
    readonly folders: ISourceFolder[];
}

如果您的源系统具有类似于类别的概念(如 CKFinder 中的“资源类型”),您可以将它们直接映射到类别。如果您的系统非常类似于文件系统,您可以将类别视为顶级文件夹。

要创建文件夹树,您需要将文件夹分配到类别中的 folders 属性中。每个文件夹都应具有其自身的标识符,该标识符在类别中是唯一的(例如,指向文件夹的路径或随机生成的 ID),范围和名称。文件夹可以包含子文件夹。

export interface ISourceFolder
    readonly id: string;
    readonly name: string;
    readonly childFolders: ISourceFolder[];
}

# 资产

每个类别都应具有

  • 唯一标识符 - 它可以是文件路径或随机生成的 ID,与文件夹相同。
  • 文件名。
  • 文件扩展名。
  • 位置(类别的 ID,以及可选的文件夹 ID)。
  • 迁移程序应用来下载文件的 URL。
  • 在您的系统中使用的 URL,应在迁移后替换为新的 URL。它可能与用于下载的 URL 相同,但它不需要相同。例如,downloadUrl 可能指向公司网络中的 API,而 downloadUrlToReplace 可能指向公开可用的端点。
export interface ISourceAsset {
    readonly id: string;
    readonly name: string;
    readonly extension: string;
    readonly location: ISourceLocation;
    readonly downloadUrl: string;
    readonly downloadUrlToReplace: string;
}

export interface ISourceLocation {
    readonly categoryId: string;
    readonly folderId?: string;
}

# 迁移资产

当迁移计划准备就绪时,迁移程序知道应该创建哪些类别、文件夹和资产,但它尚不知道文件的实际内容。要提供文件内容,您需要从 getAsset() 方法返回文件流

import fetch, { Response } from 'node-fetch';

export default class ExampleAdapter implements ISourceStorageAdapter {

    // ...

    public async getAsset( downloadUrl: string ): Promise<IGetAssetResult> {
        const response: Response = await fetch( downloadUrl );

        if ( !response.ok ) {
            throw new Error(
                `Failed to fetch file from the Example service at ${ downloadUrl }. ` +
                `Status code: ${ response.status }. ${ await response.text() }`
            );
        }

        return {
            stream: response.body,
            responsiveImages: []
        }
    }

# 响应式图像

如果您的源系统为 响应式图像 提供了 URL,那么值得在 IGetAssetResultresponsiveImages 属性中返回响应式版本的 URL。借助于此,迁移程序可以将链接添加到 URL 映射列表中。

import fetch, { Response } from 'node-fetch';

export default class ExampleAdapter implements ISourceStorageAdapter {

    // ...

    public async getAsset( downloadUrl: string ): Promise<IGetAssetResult> {
        const response: Response = await fetch( downloadUrl );

        if ( !response.ok ) {
            throw new Error(
                `Failed to fetch file from the Example service at ${ downloadUrl }. ` +
                `Status code: ${ response.status }. ${ await response.text() }`
            );
        }

        return {
            stream: response.body,
            responsiveImages: [
                {
                    width: 100,
                    url: `${ downloadUrl }/w_100`
                },
                {
                    width: 300,
                    url: `${ downloadUrl }/w_300`
                }
                // ...
            ]
        }
    }