diff --git a/src/cli.ts b/src/cli.ts index d5730d1..fabca54 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,6 +14,7 @@ const argv = yargs.options({ "state-name": { type: "string", default: ".ftp-deploy-sync-state.json" }, "dry-run": { type: "boolean", default: false, description: "Prints which modifications will be made with current config options, but doesn't actually make any changes" }, "dangerous-clean-slate": { type: "boolean", default: false, description: "Deletes ALL contents of server-dir, even items in excluded with 'exclude' argument" }, + "sync-posix-modes: { type: "boolean", default: false, description: "Sync POSIX file modes to server for new files. (Note: Only supported on POSIX compatible FTP servers.)"} "exclude": { type: "array", default: excludeDefaults, description: "An array of glob patterns, these files will not be included in the publish/delete process" }, "log-level": { choices: ["minimal", "standard", "verbose"], default: "standard", description: "How much information should print. minimal=only important info, standard=important info and basic file changes, verbose=print everything the script is doing" }, "security": { choices: ["strict", "loose"], default: "loose", description: "" } diff --git a/src/deploy.ts b/src/deploy.ts index 25421a8..6056c47 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -184,7 +184,7 @@ export async function deploy(args: IFtpDeployArgumentsWithDefaults, logger: ILog timings.start("upload"); try { - const syncProvider = new FTPSyncProvider(client, logger, timings, args["local-dir"], args["server-dir"], args["state-name"], args["dry-run"]); + const syncProvider = new FTPSyncProvider(client, logger, timings, args["local-dir"], args["server-dir"], args["state-name"], args["dry-run"], args["sync-posix-modes"]); await syncProvider.syncLocalToServer(diffs); } finally { @@ -213,4 +213,4 @@ export async function deploy(args: IFtpDeployArgumentsWithDefaults, logger: ILog logger.all(`----------------------------------------------------------------`); logger.all(`Total time: ${timings.getTimeFormatted("total")}`); logger.all(`----------------------------------------------------------------`); -} \ No newline at end of file +} diff --git a/src/main.test.ts b/src/main.test.ts index a5ce4ed..1d27dbc 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -266,7 +266,7 @@ describe("FTP sync commands", () => { ensureDir() { }, uploadFrom() { }, }; - const syncProvider = new FTPSyncProvider(mockClient as any, mockedLogger, mockedTimings, "local-dir/", "server-dir/", "state-name", false); + const syncProvider = new FTPSyncProvider(mockClient as any, mockedLogger, mockedTimings, "local-dir/", "server-dir/", "state-name", false, false); const spyRemoveFile = jest.spyOn(syncProvider, "uploadFile"); const mockClientUploadFrom = jest.spyOn(mockClient, "uploadFrom"); await syncProvider.syncLocalToServer(diffs); @@ -322,7 +322,7 @@ describe("FTP sync commands", () => { remove() { }, uploadFrom() { }, }; - const syncProvider = new FTPSyncProvider(mockClient as any, mockedLogger, mockedTimings, "local-dir/", "server-dir/", "state-name", false); + const syncProvider = new FTPSyncProvider(mockClient as any, mockedLogger, mockedTimings, "local-dir/", "server-dir/", "state-name", false, false); const spyUploadFile = jest.spyOn(syncProvider, "uploadFile"); const spyRemoveFile = jest.spyOn(syncProvider, "removeFile"); const mockClientUploadFrom = jest.spyOn(mockClient, "uploadFrom"); @@ -386,7 +386,7 @@ describe("FTP sync commands", () => { remove() { }, uploadFrom() { }, }; - const syncProvider = new FTPSyncProvider(mockClient as any, mockedLogger, mockedTimings, "local-dir/", "server-dir/", "state-name", false); + const syncProvider = new FTPSyncProvider(mockClient as any, mockedLogger, mockedTimings, "local-dir/", "server-dir/", "state-name", false, false); const spyUploadFile = jest.spyOn(syncProvider, "uploadFile"); const mockClientUploadFrom = jest.spyOn(mockClient, "uploadFrom"); await syncProvider.syncLocalToServer(diffs); @@ -435,7 +435,7 @@ describe("FTP sync commands", () => { remove() { }, uploadFrom() { }, }; - const syncProvider = new FTPSyncProvider(mockClient as any, mockedLogger, mockedTimings, "local-dir/", "server-dir/", "state-name", false); + const syncProvider = new FTPSyncProvider(mockClient as any, mockedLogger, mockedTimings, "local-dir/", "server-dir/", "state-name", false, false); const spyRemoveFile = jest.spyOn(syncProvider, "removeFile"); const mockClientRemove = jest.spyOn(mockClient, "remove"); const mockClientUploadFrom = jest.spyOn(mockClient, "uploadFrom"); @@ -493,7 +493,7 @@ describe("FTP sync commands", () => { uploadFrom() { }, cdup() { }, }; - const syncProvider = new FTPSyncProvider(mockClient as any, mockedLogger, mockedTimings, "local-dir/", "server-dir/", "state-name", false); + const syncProvider = new FTPSyncProvider(mockClient as any, mockedLogger, mockedTimings, "local-dir/", "server-dir/", "state-name", false, false); const spyRemoveFolder = jest.spyOn(syncProvider, "removeFolder"); const mockClientRemove = jest.spyOn(mockClient, "remove"); const mockClientUploadFrom = jest.spyOn(mockClient, "uploadFrom"); @@ -589,6 +589,7 @@ describe("getLocalFiles", () => { exclude: [], "log-level": "standard", security: "loose", + "sync-posix-modes": true, }); const mainYamlDiff = localDirDiffs.data.find(diff => diff.name === "workflows/main.yml")! as IFile; @@ -760,4 +761,4 @@ describe("Deploy", () => { ftpServer.close(); }, 30000); -}); \ No newline at end of file +}); diff --git a/src/syncProvider.ts b/src/syncProvider.ts index 1de126b..0f7fe01 100644 --- a/src/syncProvider.ts +++ b/src/syncProvider.ts @@ -1,6 +1,8 @@ +import { stat } from "fs"; + import prettyBytes from "pretty-bytes"; import type * as ftp from "basic-ftp"; -import { DiffResult, ErrorCode, IFilePath } from "./types"; +import { DiffResult, ErrorCode, IFilePath, Record } from "./types"; import { ILogger, pluralize, retryRequest, ITimings } from "./utilities"; export async function ensureDir(client: ftp.Client, logger: ILogger, timings: ITimings, folder: string): Promise { @@ -29,7 +31,7 @@ interface ISyncProvider { } export class FTPSyncProvider implements ISyncProvider { - constructor(client: ftp.Client, logger: ILogger, timings: ITimings, localPath: string, serverPath: string, stateName: string, dryRun: boolean) { + constructor(client: ftp.Client, logger: ILogger, timings: ITimings, localPath: string, serverPath: string, stateName: string, dryRun: boolean, syncPosixModes: boolean) { this.client = client; this.logger = logger; this.timings = timings; @@ -37,6 +39,7 @@ export class FTPSyncProvider implements ISyncProvider { this.serverPath = serverPath; this.stateName = stateName; this.dryRun = dryRun; + this.syncPosixModes = syncPosixModes; } private client: ftp.Client; @@ -45,6 +48,7 @@ export class FTPSyncProvider implements ISyncProvider { private localPath: string; private serverPath: string; private dryRun: boolean; + private syncPosixModes: boolean; private stateName: string; @@ -146,6 +150,21 @@ export class FTPSyncProvider implements ISyncProvider { this.logger.verbose(` file ${typePast}`); } + async syncMode(file: Record) { + if (this.syncPosixModes) { + // https://www.martin-brennan.com/nodejs-file-permissions-fstat/ + stat(this.localPath + file.name, (err, stats) => { + let mode: string = "0" + (stats.mode & parseInt('777', 8)).toString(8); + // https://github.com/patrickjuchli/basic-ftp/issues/9 + let command = "SITE CHMOD " + mode + " " + file.name + if (this.dryRun == false) { + this.client.ftp.send(command); + } + this.logger.verbose("Setting file mode with command " + command); + }); + } + } + async syncLocalToServer(diffs: DiffResult) { const totalCount = diffs.delete.length + diffs.upload.length + diffs.replace.length; @@ -157,11 +176,13 @@ export class FTPSyncProvider implements ISyncProvider { // create new folders for (const file of diffs.upload.filter(item => item.type === "folder")) { await this.createFolder(file.name); + this.syncMode(file); } // upload new files for (const file of diffs.upload.filter(item => item.type === "file").filter(item => item.name !== this.stateName)) { await this.uploadFile(file.name, "upload"); + this.syncMode(file); } // replace new files diff --git a/src/types.ts b/src/types.ts index b825922..5b6e6c2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -73,6 +73,7 @@ export interface IFtpDeployArgumentsWithDefaults { "server-dir": string; "state-name": string; "dry-run": boolean; + "sync-posix-modes": boolean; "dangerous-clean-slate": boolean; exclude: string[]; "log-level": "minimal" | "standard" | "verbose"; @@ -203,4 +204,4 @@ export enum ErrorCode { CannotConnectRefusedByServer = 10061, DirectoryNotEmpty = 10066, TooManyUsers = 10068, -}; \ No newline at end of file +}; diff --git a/src/utilities.ts b/src/utilities.ts index 5aed2a5..0dda6e9 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -188,6 +188,7 @@ export function getDefaultSettings(withoutDefaults: IFtpDeployArguments): IFtpDe "exclude": withoutDefaults.exclude ?? excludeDefaults, "log-level": withoutDefaults["log-level"] ?? "standard", "security": withoutDefaults.security ?? "loose", + "sync-posix-modes": false, }; } @@ -209,4 +210,4 @@ export function applyExcludeFilter(stat: IStats, excludeFilters: Readonly