Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chown recursive option #5

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
16 changes: 14 additions & 2 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1318,7 +1318,7 @@ this API: [`fs.chmod()`][].

See also: chmod(2).

## fs.chown(path, uid, gid, callback)
## fs.chown(path, uid, gid[, options], callback)
<!-- YAML
added: v0.1.97
changes:
Expand All @@ -1336,9 +1336,15 @@ changes:
it will emit a deprecation warning with id DEP0013.
-->

> Stability: 1 - Recursive chown is experimental.

* `path` {string|Buffer|URL}
* `uid` {integer}
* `gid` {integer}
* `options` {object}
* `recursive` {boolean} If `true`, perform a recursive directory chown. In
recursive mode, errors are not reported if `path` does not exist, and
operations are retried on failure. **Default:** `false`.
* `callback` {Function}
* `err` {Error}

Expand All @@ -1347,7 +1353,7 @@ possible exception are given to the completion callback.

See also: chown(2).

## fs.chownSync(path, uid, gid)
## fs.chownSync(path, uid, gid[, options])
<!-- YAML
added: v0.1.97
changes:
Expand All @@ -1357,9 +1363,15 @@ changes:
protocol. Support is currently still *experimental*.
-->

> Stability: 1 - Recursive removal is experimental.

* `path` {string|Buffer|URL}
* `uid` {integer}
* `gid` {integer}
* `options` {Object}
* `recursive` {boolean} If `true`, perform a recursive directory chown. In
recursive mode, errors are not reported if `path` does not exist, and
operations are retried on failure. **Default:** `false`.

Synchronously changes owner and group of a file. Returns `undefined`.
This is the synchronous version of [`fs.chown()`][].
Expand Down
32 changes: 29 additions & 3 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ const {
validateOffsetLengthRead,
validateOffsetLengthWrite,
validatePath,
validateChownOptions,
validateRmdirOptions,
warnOnNonPortableTemplate
warnOnNonPortableTemplate,
} = require('internal/fs/utils');
const {
CHAR_FORWARD_SLASH,
Expand All @@ -103,6 +104,8 @@ let ReadStream;
let WriteStream;
let rimraf;
let rimrafSync;
let chownR;
let chownRSync;

// These have to be separate because of how graceful-fs happens to do it's
// monkeypatching.
Expand Down Expand Up @@ -745,6 +748,12 @@ function lazyLoadRimraf() {
({ rimraf, rimrafSync } = require('internal/fs/rimraf'));
}

function lazyLoadChownR() {
if (chownR === undefined)
({ chownR,
chownRSync } = require('internal/fs/chown_recursive'));
}

function rmdir(path, options, callback) {
if (typeof options === 'function') {
callback = options;
Expand Down Expand Up @@ -1170,21 +1179,38 @@ function fchownSync(fd, uid, gid) {
handleErrorFromBinding(ctx);
}

function chown(path, uid, gid, callback) {
function chown(path, uid, gid, options, callback) {
callback = makeCallback(callback);
path = getValidatedPath(path);
validateUint32(uid, 'uid');
validateUint32(gid, 'gid');

callback = makeCallback(callback);
path = pathModule.toNamespacedPath(getValidatedPath(path));
options = validateChownOptions(options);

if (options.recursive) {
lazyLoadChownR();
chownR(path, uid, gid, options, callback);
}

const req = new FSReqCallback();
req.oncomplete = callback;
binding.chown(pathModule.toNamespacedPath(path), uid, gid, req);
}

function chownSync(path, uid, gid) {
function chownSync(path, uid, gid, options) {
path = getValidatedPath(path);
validateUint32(uid, 'uid');
validateUint32(gid, 'gid');

options = validateChownOptions(options);

if (options.recursive) {
lazyLoadChownR();
chownRSync(path, uid, gid, options);
}

const ctx = { path };
binding.chown(pathModule.toNamespacedPath(path), uid, gid, undefined, ctx);
handleErrorFromBinding(ctx);
Expand Down
106 changes: 106 additions & 0 deletions lib/internal/fs/chown_recursive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
'use strict';

const {
readdir,
readdirSync,
lstat,
lstatSync,
chown,
chownSync
} = require('fs');

const { join, resolve } = require('path');
const notEmptyErrorCodes = new Set(['EEXIST']);
const { setTimeout } = require('timers');

function chownRSync(path, uid, gid, options) {
const stats = lstatSync(path);
if (stats !== undefined && stats.isDirectory()) {
const childrenPaths = readdirSync(path, { withFileTypes: true });
childrenPaths.forEach((childPath) =>
_chownRChildrenSync(path, childPath, uid, gid, options));
} else {
chownSync(path, uid, gid);
}
}

function _chownRChildrenSync(path, childPath, uid, gid, options) {
if (typeof childPath === 'string') {
const stats = lstatSync(resolve(path, childPath));
stats.name = childPath;
childPath = stats;
}

if (childPath.isDirectory()) {
chownRSync(resolve(path, childPath.name), uid, gid, options);
}

chownSync(resolve(path, childPath.name), uid, gid);
}

function _chownChildren(path, uid, gid, options, callback) {
readdir(path, (err, files) => {
if (err)
return callback(err);

let numFiles = files.length;

if (numFiles === 0)
return chown(path, uid, gid, callback);

let done = false;

files.forEach((child) => {
chownR(join(path, child), uid, gid, options, (err) => {
if (done)
return;

if (err) {
done = true;
return callback(err);
}

numFiles--;
if (numFiles === 0)
chown(path, uid, gid, callback);
});
});
});
}

function _chown(path, uid, gid, options, originalErr, callback) {
chown(path, (err) => {
if (err) {
if (notEmptyErrorCodes.has(err.code))
return _chownChildren(path, uid, gid, options, callback);
if (err.code === 'ENOTDIR')
return callback(originalErr);
}
callback(err);
});
}

function _chownR(path, uid, gid, options, callback) {
lstat(path, (err, stats) => {
if (err) {
if (err.code === 'ENOENT')
return callback(null);
} else if (stats.isDirectory()) {
return _chown(path, uid, gid, options, err, callback);
}
});
}

function chownR(path, uid, gid, options, callback) {
let timeout = 0; // For EMFILE handling.
_chownR(path, uid, gid, options, function CB(err) {
if (err) {
if (err.code === 'EMFILE')
return setTimeout(_chownR, timeout++, path, options, CB);
}
callback(err);
});
}


module.exports = { chownRSync, chownR };
27 changes: 24 additions & 3 deletions lib/internal/fs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,26 @@ const validateRmdirOptions = hideStackFrames((options) => {
return options;
});

// @TODO: Add additional options to match chown cli utility.
const defaultChownOptions = {
recursive: false,
};

const validateChownOptions = hideStackFrames((options) => {
if (options === undefined)
return defaultChownOptions;
if (options === null || typeof options !== 'object')
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);

options = { ...defaultChownOptions, ...options };

if (typeof options.recursive !== 'boolean')
throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', options.recursive);

return options;

});


module.exports = {
assertEncoding,
Expand All @@ -560,19 +580,20 @@ module.exports = {
Dirent,
getDirents,
getOptions,
getStatsFromBinding,
getValidatedPath,
nullCheck,
preprocessSymlinkDestination,
realpathCacheKey: Symbol('realpathCacheKey'),
getStatsFromBinding,
Stats,
stringToFlags,
stringToSymlinkType,
Stats,
toUnixTimestamp,
validateBufferArray,
validateChownOptions,
validateOffsetLengthRead,
validateOffsetLengthWrite,
validatePath,
validateRmdirOptions,
warnOnNonPortableTemplate
warnOnNonPortableTemplate,
};
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
'lib/internal/fs/promises.js',
'lib/internal/fs/read_file_context.js',
'lib/internal/fs/rimraf.js',
'lib/internal/fs/chown_recursive.js',
'lib/internal/fs/streams.js',
'lib/internal/fs/sync_write_stream.js',
'lib/internal/fs/utils.js',
Expand Down
Loading