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

Add a file "rename" event #303

Closed
jeremydiviney opened this issue Jun 15, 2015 · 40 comments
Closed

Add a file "rename" event #303

jeremydiviney opened this issue Jun 15, 2015 · 40 comments

Comments

@jeremydiviney
Copy link

would be very conveinent if chokidar emitted 'rename' events when a file is renamed. Currently you get an "add" and "unlink" event, and it is challenging to detect that a file was renamed. Especially because it seems that the "unlink" and "add" events don't always emit in the same order when renaming a file.

@es128
Copy link
Contributor

es128 commented Jun 15, 2015

Can't get this consistently from the underlying APIs (primarily from node core), and replacing the current add/unlink with a rename would be a breaking change. Keeping the behavior as consistent as we can regardless of platform is a high priority, so even in the circumstances where this is possible it's unlikely to get exposed by chokidar.

@es128
Copy link
Contributor

es128 commented Jun 15, 2015

Open to ideas though, and maybe there is a strategy that would allow this to be added as a non-breaking change (such as being enabled by an option or emitting rename in addition to the other two existing events).

@CxRes
Copy link

CxRes commented Aug 2, 2015

Since I desperately could do with a rename event as well, may I suggest at the very least you can add a rename argument to add and unlink events as a boolean. Then, if you add a rename event, the user may just choose to add a empty return if rename flag is true in the onAdd and onUnlink routines and let it be handled by the rename event's onRename callback.
For those who don't care, the api does not break as add and delete are still emitted! Also gives the user flexibility to let common code between add/unlink and rename to remain in the former.

@paulmillr
Copy link
Owner

I think it is possible now to make the event on your own. Try something like this:

var recentlyMoved = {};
var recentlyMovedTimers = {};

var removeRecentlyMoved = function(path) {
  if (!recentlyMovedTimers[path]) return;
  clearTimeout(recentlyMovedTimers[path]);
  delete recentlyMovedTimers[path]
  delete removeRecentlyMoved[path];
};

var checkIfWasMoved = function(path) {
  if (recentlyMoved[path]) {
    emit('rename', path);
    removeRecentlyMoved(path);
  } else {
    recentlyMoved[path] = true;
    recentlyMovedTimers[path] = setTimeout(removeRecentlyMoved.bind(null, path), 1000);
  }
};

watcher.on('add', checkIfWasMoved);
watcher.on('remove', checkIfWasMoved);

@es128
Copy link
Contributor

es128 commented Aug 2, 2015

@CxRes that's a really good idea for how to handle the API when the data is available. I remain concerned that it will be difficult to relate the corresponding rename, add and unlink events deterministically. But this is now on my to-do list of stuff to try out. And PR welcome if anyone else can figure it out sooner.

@paulmillr it's a nice idea, but also non-deterministic I'm afraid. Looks very vulnerable to races and edge cases.

@CxRes
Copy link

CxRes commented Aug 2, 2015

@es128 I too am having second thoughts about having code common between add & rename or unlink & rename. Well, a little code repetition is a small price to pay for this!!!

But, I think there is no problem, if the user is prudent enough to do the following as long as Chokidar can guarantee that oldfile is becoming newfile . I do not see see a race condition since only one block will execute. However, knowing if this condition (oldfile -> newfile) can be met is your area of expertise:

watcher.on('add', function (newfile,stat,rename){
  if (rename) return;
  else{...}
});

watcher.on('unlink', function (oldfile,rename){
  if (rename) return;
  else{...}
});

watcher.on('rename', function (oldfile,newfile,stat){
 //manage your rename event
});

On an unrelated note, I find that having addDir/UnlinkDir somehow unstatisfactory - given that on this occasion you will have to write renameDir as well. I end up using same call back for Dir and Files on many occasions and certainly see that happen for rename. I wonder why that option was taken instead of using the stat object to detect directories (imho, though I may be wrong saying this: even unlink/change should stat the historical object!)

@es128
Copy link
Contributor

es128 commented Aug 2, 2015

Code repitition was not my concern - it's controlling the flow of events such that chokidar can guarantee that the add and unlink related to a rename wind up being flagged as such, as each will be detected along a separate flows of logic. I'm talking about how it would be within chokidar's code, not the user-facing perspective.

It's not like chokidar is taking underlying rename events now and translating that to the two separate events - if that were the case this would probably be easier. I don't know since I haven't really examined the situation yet, but it may be hard to do this without resorting to artificial delays and other such undesirable artifacts of supporting this feature. I do intend to spend some time finding out sooner or later though.

@CxRes
Copy link

CxRes commented Aug 3, 2015

Oh ok! I am afraid that even after staring at the code I do not understand the internals. Thanks for looking into it and would really appreciate a rename event soon.

@es128
Copy link
Contributor

es128 commented Aug 3, 2015

You could try @paulmillr's sample code to begin with and see if it works out for your particular use-case.

I wouldn't expect this to be resolved very soon within chokidar (not by me, anyway).

@Xample
Copy link

Xample commented Aug 14, 2015

same needs here, or if only the events were always arriving in the same order…

@nono
Copy link
Contributor

nono commented Dec 14, 2015

I'm also interested by a "rename" event.

@es128 @paulmillr the sample code does not work. If I understand correctly, it detects files that are created and removed in the same second (and deleted and recreated).

@paulmillr
Copy link
Owner

@nono the problem is: there is no "file system level" rename event per se, for all platforms and cases. So, this may be the best thing we can create

@nono
Copy link
Contributor

nono commented Dec 14, 2015

Well, it's a bit more complicated than that:

  • With inotify, there are IN_MOVED_FROM and IN_MOVED_TO attribute,
  • The fsevents lib on npm has moved-in and moved-out events,
  • For windows, I don't know,
  • When using polling, it's probably very difficult to detect move/rename in a reliable way.

I understand that it needs a lot of code and effort to have something usable and that masks the differences between the platform. Maybe using timers is a good way for handling renames. But I still think it should be included in chokidar, as it's not easy to get it right.

It's like awaitWriteFinish: it's code that can live out of the chokidar repository. But the first version was buggy and contributions from the community helped to fix the bugs.

@es128
Copy link
Contributor

es128 commented Dec 14, 2015

@nono I think it would be ok to explore ways to optionally expose the event in the modes/platforms where it is already made available by the lower-level layers, but then we have to take great care to reconcile the inconsistencies in behavior that could lead to.

I do not think it will be possible to successfully shim this for situations like polling that do not provide the information.

So a rename event may be useful for situations where the environment is controlled/known and supportive of reporting renames, but if building cross-compatible applications, then it will continue to be necessary to account for cases where renames are reported as unlink and add.

@zhirzh
Copy link

zhirzh commented Oct 16, 2016

What about using inode numbers? We can compare them to check whether the paths of subsequent add and unlink events point to the same file, i.e., have same inode number.

I know and I have checked that inode numbers stay the same for the following processes on both, files and directories under *nix environment:

  • cp - copy
  • mv - move
  • rename - rename
  • ln -s - symbolic link

Files only:

  • ln - hard links

I am certain that windows doesn't have inode numbers and I strongly believe that windows lacks an alternative to it (please prove me wrong, internet).

@popod
Copy link

popod commented Mar 27, 2017

Any update on this issue ? I've the same request: I would like to know if a files is renamed/moved or added/removed..

For infos: it seems that fs.watch has a "rename" event.

@paulmillr
Copy link
Owner

WONTFIX. Unreliable. Feel free to implement it on the user side.

@zikaari
Copy link

zikaari commented Mar 30, 2017

I wonder why no one paid attention to @zhirzh comment:

What about using inode numbers? We can compare them to check whether the paths of subsequent add and unlink events point to the same file, i.e., have same inode number.

I tested stating a file before and after rename on Windows, macOS and Ubuntu. inode numbers didn't change!

Seems absolutely legit, what is stopping us?

@paulmillr
Copy link
Owner

Feel free to fork chokidar and send a pull request. If it gets used and tested reliably, that could be fine.

@nono
Copy link
Contributor

nono commented Mar 30, 2017

@NeekSandhu you should also test that inode numbers are not reused. IIRC, you can safely use them on Linux, but Windows reuse them (ie, if a file is deleted and another file is added just after that, they can have the same inode number even if they are not the same file).

@CxRes
Copy link

CxRes commented Mar 30, 2017

@nono @NeekSandhu How about using inode and ctime together? If inode gets reused surely the ctime changes unless it is a rename? At least one might reasonably expect that...

@zikaari
Copy link

zikaari commented Mar 30, 2017

@nono Great discovery.
@CxRes Did you mean birthtime instead?

But how do we make sure we don't break the API. Since having this will no longer emit unlink events (by design) when any entity is renamed.

@CxRes
Copy link

CxRes commented Mar 31, 2017

@NeekSandhu You are right! I did not know that fsStat gave us birthtime as well.

regarding API: I had made one suggestion above which @es128 agrees with. Alternatively one can have a boolean flag detectRenames like @NeekSandhu suggests (in the unedited comment).

@arxpoetica
Copy link

My two cents. The API should absolutely be broken. Chock it up (no pun intended) to what a library is supposed to do: upgrade semver when the need is real. This is a very appropriate, and I would argue, correct example to break the API over.

@glortho
Copy link

glortho commented Dec 18, 2017

My 2¢: After doing lots of research I can say that cross-platform rename in node leaves much to be desired. We couldn't find a way to do it without timeouts, which creates dangerous race conditions.

FWIW we went with https://github.com/gorakhargosh/watchdog, which emits a moved event along with old and new path (using kqueue/fsevents on Mac, inotify on Linux, etc. along the lines @nono's comment #303 (comment))

@fabiospampinato
Copy link

I've implemented this here, it was somewhat tricky as the order of the "add" and "unlink" event is not consistent across platforms. I'm probably going to publish it on NPM for easier consumption in the future.

@nono
Copy link
Contributor

nono commented Jan 23, 2019

For your information, several files/folders can have the same ino on windows because of rounding issues of large numbers in JS. More details on nodejs/node#12115

@fabiospampinato
Copy link

@nono thanks for pointing that out! It seems the problem would be solvable if the stats object were to include bigints, any way to make sure chokidar provides me stats objects with bigints?

@nono
Copy link
Contributor

nono commented Jan 23, 2019

You can use make a pull request for making chokidar calls fs.stat with the bigint: true option. And be sure to use a recent version of nodejs, this option was introduced in node v10.5.0.

@manav5hah
Copy link

manav5hah commented Aug 23, 2021

Using chokidar with Node

All the fs.stats() provides ino property (inode) for all the OS.
The ino index of the file does not change with the rename operation.

File rename event can be obtained with the use of change event and fs (file-systems) module.

The change event is periodically called by chokidar.
By attaching a check of fs.exists() with the event, we can detect the operation rename of the file system.

Here's a short piece of code to do the same

var detectRename = function(pathRenamed) {
    if (fs.existsSync(pathRenamed)) {
        return false;
    }
    else {
        return renameFileSystemObject(pathRenamed);
        this.emit("rename", pathRenamed)
    } 
};

watcher.on('change', detectRename);

@fabiospampinato
Copy link

@manav5hah there are some subtleties to be aware of, a simplistic approach like that doesn't actually work:

  • First of all one must ask fs.stats to give you bigint ino numbers, otherwise ino numbers under Windows may overflow the range of regular JS integers that can be represented faithfully, making them unreliable for our needs.
  • Secondly you can't just check if the file still exists on "change", you'd have no idea what the file got renamed into, making your rename event as useful as a "delete" event basically, you need to listen for a pair of events happening at different paths where the files have the same ino numbers.

I've sort of rewritten chokidar but with rename detection support btw, the best option is probably to just use my watcher if you need that: https://github.com/fabiospampinato/watcher

@fang-guang
Copy link

if event is "unlink", how i get fs.stats???

@fang-guang
Copy link

I think it is possible now to make the event on your own. Try something like this:

var recentlyMoved = {};
var recentlyMovedTimers = {};

var removeRecentlyMoved = function(path) {
  if (!recentlyMovedTimers[path]) return;
  clearTimeout(recentlyMovedTimers[path]);
  delete recentlyMovedTimers[path]
  delete removeRecentlyMoved[path];
};

var checkIfWasMoved = function(path) {
  if (recentlyMoved[path]) {
    emit('rename', path);
    removeRecentlyMoved(path);
  } else {
    recentlyMoved[path] = true;
    recentlyMovedTimers[path] = setTimeout(removeRecentlyMoved.bind(null, path), 1000);
  }
};

watcher.on('add', checkIfWasMoved);
watcher.on('remove', checkIfWasMoved);

i think "add" and "remove" events path is different ,can you check by path?

@lindenkron
Copy link

@manav5hah there are some subtleties to be aware of, a simplistic approach like that doesn't actually work:

* First of all one must ask `fs.stats` to give you bigint ino numbers, otherwise ino numbers under Windows may overflow the range of regular JS integers that can be represented faithfully, making them unreliable for our needs.

* Secondly you can't just check if the file still exists on "change", you'd have no idea what the file got renamed into, making your rename event as useful as a "delete" event basically, you need to listen for a pair of events happening at different paths where the files have the same ino numbers.

I've sort of rewritten chokidar but with rename detection support btw, the best option is probably to just use my watcher if you need that: https://github.com/fabiospampinato/watcher

Watcher seems to only support ESM, not CommonJS. And as it stands, I believe this won't work in Electron due to Electron only supporting CommonJS. At least until 28 is released as stable.

Am I missing something here?

@fabiospampinato
Copy link

fabiospampinato commented Sep 17, 2023

@lindenkron I use it in Electron myself, so I guess it works there? Maybe you are missing a build step or something?

@lindenkron
Copy link

@lindenkron I use it in Electron myself, so I guess it works there? Maybe you are missing a build step or something?

Hmm, perhaps. I attempted to use Watcher with require but it stated it was an ES Module and needed to use Import, which Electron doesn't support. At least I couldn't get Electron to do it, and ESM isn't supported until v28 as per:
electron/electron#21457

You are using Watcher with CommonJS?

Finding it odd that I would get an error like that only when adding watcher, but Chokidar had no issues with CommonJS.

Not sure what else I can try, but thanks for the quick reply 🙏

@fabiospampinato
Copy link

You should probably open an issue in watcher, this isn't the right place for this.

But basically watcher doesn't support commonjs, you'd have to import it. If you don't need it immediately you can also import() it from commonjs I think. I am bundling my main process code to cjs I think.

@benmccann
Copy link
Contributor

benmccann commented May 20, 2024

I know and I have checked that inode numbers stay the same for the following processes on both, files and directories under *nix environment

I'd imagine this might have the possibility to vary by file system, so it'd probably be good to specify whether tested on ext4, btrfs, etc.

And of course if you move a file to a different disk or something it will end up with a new inode, which is a caveat to document.

@benmccann
Copy link
Contributor

benmccann commented May 20, 2024

My 2¢: After doing lots of research I can say that cross-platform rename in node leaves much to be desired. We couldn't find a way to do it without timeouts, which creates dangerous race conditions.

I don't think that's Node-specific, but is due to how inoitfy works. See fabiospampinato/watcher#43 where I just did some investigation of this

FWIW we went with https://github.com/gorakhargosh/watchdog, which emits a moved event along with old and new path (using kqueue/fsevents on Mac, inotify on Linux, etc. along the lines @nono's comment #303 (comment))

I would expect they're using timeouts as well.

$ git clone [email protected]:gorakhargosh/watchdog.git
$ cd watchdog
$ grep -ri timeout . | wc -l
188

@benmccann
Copy link
Contributor

I think that it could be done without timeouts if using fanotify rather than inotify:

The event FAN_RENAME may contain two information records. One of type FAN_EVENT_INFO_TYPE_OLD_DFID_NAME identifying the old directory entry, and another of type FAN_EVENT_INFO_TYPE_NEW_DFID_NAME identifying the new directory entry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests