Skip to content

Commit

Permalink
Add support for syncing file modes
Browse files Browse the repository at this point in the history
  • Loading branch information
timthelion committed Feb 13, 2022
1 parent f79bd10 commit 2b07bcf
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 12 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ To list all commands with examples simply run `ftp-deploy` without any options.
| `--server-dir` | No | `ftp.samkirkland.com/` | `./` | Folder to upload from, must end with trailing slash `/` |
| `--state-name` | No | `folder/.sync-state.json` | `.ftp-deploy-sync-state.json` | ftp-deploy uses this file to track what's been deployed already, so only differences can be published. If you don't like the name or location you can customize it |
| `--dry-run` | No | `true` | `false` | Prints which modifications will be made with current config options, but doesn't actually make any changes |
| `--sync-posix-modes` | No | `true` | `false` | Tries to sync posix file modes between host and server. |
| `--dangerous-clean-slate` | No | `true` | `false` | Deletes ALL contents of server-dir, even items marked as `--exclude` argument |
| `--exclude` | No | `nuclearLaunchCodes.txt` | `**/.git*` `**/.git*/**` `**/node_modules/**` | An array of glob patterns, these files will not be included in the publish/delete process |
| `--log-level` | No | `info` | `info` | `minimal`: only important info, `standard`: important info and basic file changes, `verbose`: print everything the script is doing |
Expand Down
1 change: 1 addition & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "" }
Expand Down
4 changes: 2 additions & 2 deletions src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -213,4 +213,4 @@ export async function deploy(args: IFtpDeployArgumentsWithDefaults, logger: ILog
logger.all(`----------------------------------------------------------------`);
logger.all(`Total time: ${timings.getTimeFormatted("total")}`);
logger.all(`----------------------------------------------------------------`);
}
}
13 changes: 7 additions & 6 deletions src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -760,4 +761,4 @@ describe("Deploy", () => {

ftpServer.close();
}, 30000);
});
});
30 changes: 28 additions & 2 deletions src/syncProvider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import fs from "fs";
import util from "util";

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

const stat = util.promisify(fs.stat);

export async function ensureDir(client: ftp.Client, logger: ILogger, timings: ITimings, folder: string): Promise<void> {
timings.start("changingDir");
logger.verbose(` changing dir to ${folder}`);
Expand All @@ -29,14 +34,15 @@ 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;
this.localPath = localPath;
this.serverPath = serverPath;
this.stateName = stateName;
this.dryRun = dryRun;
this.syncPosixModes = syncPosixModes;
}

private client: ftp.Client;
Expand All @@ -45,6 +51,7 @@ export class FTPSyncProvider implements ISyncProvider {
private localPath: string;
private serverPath: string;
private dryRun: boolean;
private syncPosixModes: boolean;
private stateName: string;


Expand Down Expand Up @@ -146,6 +153,22 @@ export class FTPSyncProvider implements ISyncProvider {
this.logger.verbose(` file ${typePast}`);
}

async syncMode(file: Record) {
if (!this.syncPosixModes) {
return;
}
this.logger.verbose("Syncing posix mode for file " + file.name);
// https://www.martin-brennan.com/nodejs-file-permissions-fstat/
let stats = await stat(this.localPath + file.name);
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) {
await this.client.ftp.request(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;

Expand All @@ -157,17 +180,20 @@ export class FTPSyncProvider implements ISyncProvider {
// create new folders
for (const file of diffs.upload.filter(item => item.type === "folder")) {
await this.createFolder(file.name);
await 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");
await this.syncMode(file);
}

// replace new files
for (const file of diffs.replace.filter(item => item.type === "file").filter(item => item.name !== this.stateName)) {
// note: FTP will replace old files with new files. We run replacements after uploads to limit downtime
await this.uploadFile(file.name, "replace");
await this.syncMode(file);
}

// delete old files
Expand Down
10 changes: 9 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ export interface IFtpDeployArguments {
*/
"dry-run"?: boolean;

/**
* Tries to sync posix file modes to server. Only works for new or updated files.
* Note: Not all FTP servers support settings POSIX file modes.
* @default false
*/
"sync-posix-modes"?: boolean;

/**
* Deletes ALL contents of server-dir, even items in excluded with 'exclude' argument
* @default false
Expand Down Expand Up @@ -73,6 +80,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";
Expand Down Expand Up @@ -203,4 +211,4 @@ export enum ErrorCode {
CannotConnectRefusedByServer = 10061,
DirectoryNotEmpty = 10066,
TooManyUsers = 10068,
};
};
3 changes: 2 additions & 1 deletion src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": withoutDefaults["sync-posix-modes"] ?? false,
};
}

Expand All @@ -209,4 +210,4 @@ export function applyExcludeFilter(stat: IStats, excludeFilters: Readonly<string
}

return true;
}
}

0 comments on commit 2b07bcf

Please sign in to comment.