When a file is moved (via the fs) associated File objects update their path and trigger a 'move' event

This commit is contained in:
Corey Johnson & Nathan Sobo
2012-07-23 13:34:27 -07:00
parent ab47df1987
commit 7962c8ff34
6 changed files with 93 additions and 23 deletions

View File

@@ -2,7 +2,7 @@
#import "include/cef_v8.h"
#import <Foundation/Foundation.h>
typedef void (^WatchCallback)(NSArray *);
typedef void (^WatchCallback)(NSArray *, NSString *);
@interface PathWatcher : NSObject {
int _kq;

View File

@@ -2,6 +2,7 @@
#import <sys/event.h>
#import <sys/time.h>
#import <sys/param.h>
#import <fcntl.h>
static NSMutableArray *gPathWatchers;
@@ -11,6 +12,8 @@ static NSMutableArray *gPathWatchers;
- (void)watchFileDescriptor:(int)fd;
- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback callbackId:(NSString *)callbackId;
- (void)stopWatching;
- (void)reassignFileDescriptorFor:(NSString *)path to:(NSString *)newPath;
- (bool)isAtomicWrite:(struct kevent)event;
@end
@implementation PathWatcher
@@ -194,24 +197,33 @@ static NSMutableArray *gPathWatchers;
NSNumber *fdNumber = [NSNumber numberWithInt:event.ident];
NSMutableArray *eventFlags = [NSMutableArray array];
NSString *path = [self pathForFileDescriptor:fdNumber];
if (event.fflags & NOTE_WRITE) {
[eventFlags addObject:@"modified"];
}
else if ([self isAtomicWrite:event]) {
[eventFlags addObject:@"modified"];
// The fd for the path has changed. Remove references to old fd and
// make sure the path and callbacks are linked with new fd.
@synchronized(self) {
NSDictionary *callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByFileDescriptor objectForKey:fdNumber]];
NSString *path = [self pathForFileDescriptor:fdNumber];
NSDictionary *callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByFileDescriptor objectForKey:fdNumber]];
[self unwatchPath:path callbackId:nil error:nil];
for (NSString *callbackId in [callbacks allKeys]) {
[self watchPath:path callback:[callbacks objectForKey:callbackId] callbackId:callbackId];
}
[eventFlags addObject:@"modified"];
}
}
}
else if (event.fflags & NOTE_RENAME) {
[eventFlags addObject:@"moved"];
char pathBuffer[MAXPATHLEN];
fcntl((int)event.ident, F_GETPATH, &pathBuffer);
NSString *newPath = [NSString stringWithUTF8String:pathBuffer];
[self reassignFileDescriptorFor:path to:newPath];
path = newPath;
}
@synchronized(self) {
@@ -219,7 +231,7 @@ static NSMutableArray *gPathWatchers;
for (NSString *key in callbacks) {
WatchCallback callback = [callbacks objectForKey:key];
dispatch_async(dispatch_get_main_queue(), ^{
callback(eventFlags);
callback(eventFlags, path);
});
}
}
@@ -241,4 +253,12 @@ static NSMutableArray *gPathWatchers;
return NO;
}
- (void)reassignFileDescriptorFor:(NSString *)path to:(NSString *)newPath {
@synchronized(self) {
NSNumber *fdNumber = [_fileDescriptorsByPath objectForKey:path];
[_fileDescriptorsByPath removeObjectForKey:path];
[_fileDescriptorsByPath setObject:fdNumber forKey:newPath];
}
}
@end

View File

@@ -304,7 +304,7 @@ bool NativeHandler::Execute(const CefString& name,
CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
WatchCallback callback = ^(NSArray *eventList) {
WatchCallback callback = ^(NSArray *eventList, NSString *path) {
context->Enter();
CefV8ValueList args;
@@ -317,6 +317,7 @@ bool NativeHandler::Execute(const CefString& name,
}
args.push_back(eventObject);
args.push_back(CefV8Value::CreateString(std::string([path UTF8String], [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding])));
function->ExecuteFunction(function, args, retval, e, true);
context->Exit();

View File

@@ -2,25 +2,24 @@ File = require 'file'
fs = require 'fs'
describe 'File', ->
file = null
[path, file] = []
beforeEach ->
path = fs.join(require.resolve('fixtures'), "temp.txt")
path = fs.join(require.resolve('fixtures'), "atom-file-test.txt") # Don't put in /tmp because /tmp symlinks to /private/tmp and screws up the rename test
fs.remove(path) if fs.exists(path)
fs.write(path, "this is old!")
file = new File(path)
afterEach ->
file.off()
fs.remove(file.getPath()) if fs.exists(file.getPath())
fs.remove(path) if fs.exists(path)
describe "when the contents of the file change", ->
it "triggers 'contents-change' event handlers", ->
changeHandler = null
runs ->
changeHandler = jasmine.createSpy('changeHandler')
file.on 'contents-change', changeHandler
fs.write(file.getPath(), "this is new!")
changeHandler = jasmine.createSpy('changeHandler')
file.on 'contents-change', changeHandler
fs.write(file.getPath(), "this is new!")
waitsFor "change event", ->
changeHandler.callCount > 0
@@ -31,3 +30,46 @@ describe 'File', ->
waitsFor "second change event", ->
changeHandler.callCount > 0
describe "when a file is moved (via the filesystem)", ->
newPath = null
beforeEach ->
newPath = fs.join(fs.directory(path), "atom-file-was-moved-test.txt")
afterEach ->
fs.remove(newPath) if fs.exists(newPath)
it "it updates its path", ->
moveHandler = null
moveHandler = jasmine.createSpy('moveHandler')
file.on 'move', moveHandler
fs.move(path, newPath)
waitsFor "move event", ->
moveHandler.callCount > 0
runs ->
expect(file.getPath()).toBe newPath
it "maintains 'contents-change' events set on previous path", ->
moveHandler = null
moveHandler = jasmine.createSpy('moveHandler')
file.on 'move', moveHandler
changeHandler = null
changeHandler = jasmine.createSpy('changeHandler')
file.on 'contents-change', changeHandler
fs.move(path, newPath)
waitsFor "move event", ->
moveHandler.callCount > 0
runs ->
expect(changeHandler).not.toHaveBeenCalled()
fs.write(file.getPath(), "this is new!")
waitsFor "change event", ->
changeHandler.callCount > 0

View File

@@ -610,14 +610,15 @@ describe "TreeView", ->
describe "when the directories along the new path don't exist", ->
it "creates the target directory before moving the file", ->
runs ->
newPath = fs.join(rootDirPath, 'new/directory', 'renamed-test-file.txt')
moveDialog.miniEditor.setText(newPath)
rootView.project.destroy() # Ensure there are no open buffers (renaming a file asynchronously updates the buffer's path, this causes the afterEach block to unwatch the previous path, which no longer exists.)
moveDialog.trigger 'tree-view:confirm'
newPath = fs.join(rootDirPath, 'new/directory', 'renamed-test-file.txt')
moveDialog.miniEditor.setText(newPath)
expect(fs.exists(newPath)).toBeTruthy()
expect(fs.exists(filePath)).toBeFalsy()
moveDialog.trigger 'tree-view:confirm'
expect(fs.exists(newPath)).toBeTruthy()
expect(fs.exists(filePath)).toBeFalsy()
describe "when a file or directory already exists at the target path", ->
it "shows an error message and does not close the dialog", ->

View File

@@ -11,6 +11,8 @@ class File
constructor: (@path) ->
@updateMd5()
setPath: (@path) ->
getPath: ->
@path
@@ -27,7 +29,11 @@ class File
@unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0
subscribeToNativeChangeEvents: ->
@watchId = $native.watchPath @path, (eventTypes) =>
@watchId = $native.watchPath @path, (eventTypes, path) =>
if eventTypes.moved?
@setPath(path)
@trigger 'move'
newMd5 = fs.md5ForPath(@getPath())
if eventTypes.modified? and newMd5 != @md5
@md5 = newMd5