Skip to content

Commit

Permalink
Include file permissions in hash
Browse files Browse the repository at this point in the history
  • Loading branch information
scott-rc committed Nov 13, 2023
1 parent 480a651 commit ac2db1d
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 12 deletions.
228 changes: 228 additions & 0 deletions spec/services/filesync/__snapshots__/directory.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,231 @@ exports[`Directory.hashes > produces the expected result 1`] = `
"yarn.lock": "c4803c72d2e4888b4378fbc2cf2e1a1042f77d3c",
}
`;

exports[`Directory.hashes > produces the expected result 2`] = `
{
".gadget/": "755",
".gadget/client/": "755",
".gadget/client/dist-cjs/": "755",
".gadget/client/dist-cjs/Client.js": "644",
".gadget/client/dist-cjs/Client.js.map": "644",
".gadget/client/dist-cjs/iife-export.js": "644",
".gadget/client/dist-cjs/iife-export.js.map": "644",
".gadget/client/dist-cjs/index.js": "644",
".gadget/client/dist-cjs/index.js.map": "644",
".gadget/client/dist-cjs/models/": "755",
".gadget/client/dist-cjs/models/CurrentSession.js": "644",
".gadget/client/dist-cjs/models/CurrentSession.js.map": "644",
".gadget/client/dist-cjs/models/Session.js": "644",
".gadget/client/dist-cjs/models/Session.js.map": "644",
".gadget/client/dist-cjs/models/User.js": "644",
".gadget/client/dist-cjs/models/User.js.map": "644",
".gadget/client/dist-cjs/package.json": "644",
".gadget/client/dist-cjs/support.js": "644",
".gadget/client/dist-cjs/support.js.map": "644",
".gadget/client/dist-cjs/types.js": "644",
".gadget/client/dist-cjs/types.js.map": "644",
".gadget/client/dist-esm/": "755",
".gadget/client/dist-esm/Client.js": "644",
".gadget/client/dist-esm/Client.js.map": "644",
".gadget/client/dist-esm/iife-export.js": "644",
".gadget/client/dist-esm/iife-export.js.map": "644",
".gadget/client/dist-esm/index.js": "644",
".gadget/client/dist-esm/index.js.map": "644",
".gadget/client/dist-esm/models/": "755",
".gadget/client/dist-esm/models/CurrentSession.js": "644",
".gadget/client/dist-esm/models/CurrentSession.js.map": "644",
".gadget/client/dist-esm/models/Session.js": "644",
".gadget/client/dist-esm/models/Session.js.map": "644",
".gadget/client/dist-esm/models/User.js": "644",
".gadget/client/dist-esm/models/User.js.map": "644",
".gadget/client/dist-esm/package.json": "644",
".gadget/client/dist-esm/support.js": "644",
".gadget/client/dist-esm/support.js.map": "644",
".gadget/client/dist-esm/types.js": "644",
".gadget/client/dist-esm/types.js.map": "644",
".gadget/client/package.json": "644",
".gadget/client/src/": "755",
".gadget/client/src/Client.ts": "644",
".gadget/client/src/iife-export.ts": "644",
".gadget/client/src/index.ts": "644",
".gadget/client/src/models/": "755",
".gadget/client/src/models/CurrentSession.ts": "644",
".gadget/client/src/models/Session.ts": "644",
".gadget/client/src/models/User.ts": "644",
".gadget/client/src/support.ts": "644",
".gadget/client/src/types.ts": "644",
".gadget/client/tsconfig.json": "644",
".gadget/client/types/": "755",
".gadget/client/types/Client.d.ts": "644",
".gadget/client/types/iife-export.d.ts": "644",
".gadget/client/types/index.d.ts": "644",
".gadget/client/types/models/": "755",
".gadget/client/types/models/CurrentSession.d.ts": "644",
".gadget/client/types/models/Session.d.ts": "644",
".gadget/client/types/models/User.d.ts": "644",
".gadget/client/types/support.d.ts": "644",
".gadget/client/types/types.d.ts": "644",
".gadget/server/": "755",
".gadget/server/dist/": "755",
".gadget/server/dist/AccessControlMetadata.d.ts": "644",
".gadget/server/dist/AccessControlMetadata.js": "644",
".gadget/server/dist/AccessControlMetadata.js.map": "644",
".gadget/server/dist/AmbientContext.d.ts": "644",
".gadget/server/dist/AmbientContext.js": "644",
".gadget/server/dist/AmbientContext.js.map": "644",
".gadget/server/dist/AppConfigs.d.ts": "644",
".gadget/server/dist/AppConfigs.js": "644",
".gadget/server/dist/AppConfigs.js.map": "644",
".gadget/server/dist/AppConfiguration.d.ts": "644",
".gadget/server/dist/AppConfiguration.js": "644",
".gadget/server/dist/AppConfiguration.js.map": "644",
".gadget/server/dist/AppConnections.d.ts": "644",
".gadget/server/dist/AppConnections.js": "644",
".gadget/server/dist/AppConnections.js.map": "644",
".gadget/server/dist/Session.d.ts": "644",
".gadget/server/dist/Session.js": "644",
".gadget/server/dist/Session.js.map": "644",
".gadget/server/dist/ai/": "755",
".gadget/server/dist/ai/index.d.ts": "644",
".gadget/server/dist/ai/index.js": "644",
".gadget/server/dist/ai/index.js.map": "644",
".gadget/server/dist/auth.d.ts": "644",
".gadget/server/dist/auth.js": "644",
".gadget/server/dist/auth.js.map": "644",
".gadget/server/dist/effects.d.ts": "644",
".gadget/server/dist/effects.js": "644",
".gadget/server/dist/effects.js.map": "644",
".gadget/server/dist/email-templates/": "755",
".gadget/server/dist/email-templates/index.d.ts": "644",
".gadget/server/dist/email-templates/index.js": "644",
".gadget/server/dist/email-templates/index.js.map": "644",
".gadget/server/dist/email-templates/reset-password.d.ts": "644",
".gadget/server/dist/email-templates/reset-password.js": "644",
".gadget/server/dist/email-templates/reset-password.js.map": "644",
".gadget/server/dist/email-templates/verify-email.d.ts": "644",
".gadget/server/dist/email-templates/verify-email.js": "644",
".gadget/server/dist/email-templates/verify-email.js.map": "644",
".gadget/server/dist/emails.d.ts": "644",
".gadget/server/dist/emails.js": "644",
".gadget/server/dist/emails.js.map": "644",
".gadget/server/dist/errors.d.ts": "644",
".gadget/server/dist/errors.js": "644",
".gadget/server/dist/errors.js.map": "644",
".gadget/server/dist/global-actions.d.ts": "644",
".gadget/server/dist/global-actions.js": "644",
".gadget/server/dist/global-actions.js.map": "644",
".gadget/server/dist/globals.d.ts": "644",
".gadget/server/dist/globals.js": "644",
".gadget/server/dist/globals.js.map": "644",
".gadget/server/dist/index.d.ts": "644",
".gadget/server/dist/index.js": "644",
".gadget/server/dist/index.js.map": "644",
".gadget/server/dist/metadata.d.ts": "644",
".gadget/server/dist/metadata.js": "644",
".gadget/server/dist/metadata.js.map": "644",
".gadget/server/dist/models/": "755",
".gadget/server/dist/models/Session.d.ts": "644",
".gadget/server/dist/models/Session.js": "644",
".gadget/server/dist/models/Session.js.map": "644",
".gadget/server/dist/models/User.d.ts": "644",
".gadget/server/dist/models/User.js": "644",
".gadget/server/dist/models/User.js.map": "644",
".gadget/server/dist/nodemailer-transports.d.ts": "644",
".gadget/server/dist/nodemailer-transports.js": "644",
".gadget/server/dist/nodemailer-transports.js.map": "644",
".gadget/server/dist/routes.d.ts": "644",
".gadget/server/dist/routes.js": "644",
".gadget/server/dist/routes.js.map": "644",
".gadget/server/dist/state-chart/": "755",
".gadget/server/dist/state-chart/StateMapper.d.ts": "644",
".gadget/server/dist/state-chart/StateMapper.js": "644",
".gadget/server/dist/state-chart/StateMapper.js.map": "644",
".gadget/server/dist/state-chart/index.d.ts": "644",
".gadget/server/dist/state-chart/index.js": "644",
".gadget/server/dist/state-chart/index.js.map": "644",
".gadget/server/dist/tenancy.d.ts": "644",
".gadget/server/dist/tenancy.js": "644",
".gadget/server/dist/tenancy.js.map": "644",
".gadget/server/dist/types.d.ts": "644",
".gadget/server/dist/types.js": "644",
".gadget/server/dist/types.js.map": "644",
".gadget/server/dist/utils.d.ts": "644",
".gadget/server/dist/utils.js": "644",
".gadget/server/dist/utils.js.map": "644",
".gadget/server/package.json": "644",
".gadget/server/src/": "755",
".gadget/server/src/AccessControlMetadata.ts": "644",
".gadget/server/src/AmbientContext.ts": "644",
".gadget/server/src/AppConfigs.ts": "644",
".gadget/server/src/AppConfiguration.ts": "644",
".gadget/server/src/AppConnections.ts": "644",
".gadget/server/src/Session.ts": "644",
".gadget/server/src/ai/": "755",
".gadget/server/src/ai/index.ts": "644",
".gadget/server/src/auth.ts": "644",
".gadget/server/src/effects.ts": "644",
".gadget/server/src/email-templates/": "755",
".gadget/server/src/email-templates/index.ts": "644",
".gadget/server/src/email-templates/reset-password.ts": "644",
".gadget/server/src/email-templates/verify-email.ts": "644",
".gadget/server/src/emails.ts": "644",
".gadget/server/src/errors.ts": "644",
".gadget/server/src/global-actions.ts": "644",
".gadget/server/src/globals.ts": "644",
".gadget/server/src/index.ts": "644",
".gadget/server/src/metadata.d.ts": "644",
".gadget/server/src/metadata.ts": "644",
".gadget/server/src/models/": "755",
".gadget/server/src/models/Session.ts": "644",
".gadget/server/src/models/User.ts": "644",
".gadget/server/src/nodemailer-transports.ts": "644",
".gadget/server/src/routes.ts": "644",
".gadget/server/src/state-chart/": "755",
".gadget/server/src/state-chart/StateMapper.ts": "644",
".gadget/server/src/state-chart/index.ts": "644",
".gadget/server/src/tenancy.ts": "644",
".gadget/server/src/types.ts": "644",
".gadget/server/src/utils.ts": "644",
".gadget/server/tsconfig.json": "644",
"frontend/": "755",
"frontend/App.css": "755",
"frontend/App.jsx": "755",
"frontend/api.js": "755",
"frontend/assets/": "755",
"frontend/assets/default-background.svg": "755",
"frontend/assets/default-user-icon.svg": "755",
"frontend/assets/google.svg": "755",
"frontend/assets/react-logo.svg": "755",
"frontend/main.jsx": "755",
"frontend/routes/": "755",
"frontend/routes/change-password.jsx": "755",
"frontend/routes/forgot-password.jsx": "755",
"frontend/routes/index.jsx": "755",
"frontend/routes/reset-password.jsx": "755",
"frontend/routes/sign-in.jsx": "755",
"frontend/routes/sign-up.jsx": "755",
"frontend/routes/signed-in.jsx": "755",
"frontend/routes/verify-email.jsx": "755",
"index.html": "755",
"package.json": "755",
"routes/": "755",
"routes/GET-hello.js": "755",
"user/": "755",
"user/actions/": "755",
"user/actions/changePassword.js": "755",
"user/actions/delete.js": "755",
"user/actions/resetPassword.js": "755",
"user/actions/sendResetPassword.js": "755",
"user/actions/sendVerifyEmail.js": "755",
"user/actions/signIn.js": "755",
"user/actions/signOut.js": "755",
"user/actions/signUp.js": "755",
"user/actions/update.js": "755",
"user/actions/verifyEmail.js": "755",
"user/filters/": "755",
"user/filters/tenant.gelly": "755",
"vite.config.js": "755",
"yarn.lock": "644",
}
`;
14 changes: 12 additions & 2 deletions spec/services/filesync/directory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import fs from "fs-extra";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { ALWAYS_IGNORE_PATHS, Directory, HASHING_IGNORE_PATHS } from "../../../src/services/filesync/directory.js";
import { mapValues } from "../../../src/services/collections.js";
import { ALWAYS_IGNORE_PATHS, Directory, HASHING_IGNORE_PATHS, supportsPermissions } from "../../../src/services/filesync/directory.js";
import { writeDir, type Files } from "../../__support__/files.js";
import { appFixturePath, testDirPath } from "../../__support__/paths.js";

Expand Down Expand Up @@ -308,7 +309,16 @@ describe("Directory.walk", () => {
describe("Directory.hashes", () => {
it("produces the expected result", async () => {
const directory = await Directory.init(appFixturePath());
await expect(directory.hashes()).resolves.toMatchSnapshot();
const hashes = await directory.hashes();
expect(mapValues(hashes, (hash) => hash.sha1)).toMatchSnapshot();

if (supportsPermissions) {
expect(mapValues(hashes, (hash) => hash.permissions!.toString(8))).toMatchSnapshot();
} else {
expect(mapValues(hashes, (hash) => hash.permissions)).toEqual(mapValues(hashes, () => undefined));
}

expect.assertions(2);
});
});

Expand Down
11 changes: 11 additions & 0 deletions src/services/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,14 @@ export const pick = <T extends Record<string, unknown>, K extends keyof T>(objec
}
return final;
};

export const mapValues = <Key extends string | number | symbol, Value, MappedValue>(
obj: Record<Key, Value>,
fn: (value: Value) => MappedValue,
) => {
const mapped = {} as Record<Key, MappedValue>;
for (const [key, value] of Object.entries(obj)) {
mapped[key as Key] = fn(value as Value);
}
return mapped;
};
55 changes: 45 additions & 10 deletions src/services/filesync/directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,25 +217,60 @@ export class Directory {
* Key/value pairs where the key is the normalized path and the value is
* the result of {@linkcode hash} for that path.
*/
export type Hashes = Record<string, string>;
export type Hashes = Record<string, Hash>;

export interface Hash {
/**
* The SHA-1 hash of the file or directory.
*
* If the path points to a directory, the hash is calculated based on
* the directory's basename. If the path points to a file, the hash is
* calculated based on the file's basename and contents.
*/
sha1: string;

/**
* The Unix-style file permissions of the file or directory, or
* undefined if the platform that generated this hash doesn't support
* them.
*
* @example 0o644
* @see supportsPermissions
*/
permissions?: number;
}

/**
* Whether the current platform supports Unix-style file permissions.
*
* Windows doesn't support Unix-style file permissions and all file
* permissions retrieved via `node:fs` on Windows are translated to 666
* or 444.
*/
export const supportsPermissions = process.platform === "linux" || process.platform === "darwin";

/**
* Calculates the SHA-1 hash of the file or directory at the specified
* absolute path. If the path points to a directory, the hash is
* calculated based on the directory name. If the path points to a file,
* the hash is calculated based on the file's name and contents.
* Calculates the {@linkcode Hash} of the file or directory at the
* specified absolute path.
*
* @param absolutePath The absolute path to the file or directory.
* @returns A Promise that resolves to the SHA-1 hash of the file or
* directory.
* @returns A Promise that resolves to the {@linkcode Hash} of the file
* or directory.
*/
const hash = async (absolutePath: string): Promise<string> => {
const hash = async (absolutePath: string): Promise<Hash> => {
const sha1 = createHash("sha1");
sha1.update(path.basename(absolutePath));

const stats = await fs.stat(absolutePath);

let permissions;
if (supportsPermissions) {
// strip everything but the permissions
permissions = stats.mode & 0o777;
}

if (stats.isDirectory()) {
return sha1.digest("hex");
return { sha1: sha1.digest("hex"), permissions };
}

// windows uses CRLF line endings whereas unix uses LF line endings so
Expand All @@ -258,7 +293,7 @@ const hash = async (absolutePath: string): Promise<string> => {

await pipeline(fs.createReadStream(absolutePath), removeCR, sha1);

return sha1.digest("hex");
return { sha1: sha1.digest("hex"), permissions };
};

/**
Expand Down

0 comments on commit ac2db1d

Please sign in to comment.