@@ -21,6 +21,7 @@ import type { PathLike } from '@jsonjoy.com/fs-node-utils/lib/types/misc';
2121import { ERROR_CODE } from './constants' ;
2222import { TFileId , StatError } from './types' ;
2323import { Err , Ok , Result } from './result' ;
24+ import { FsEvent , FsEventType } from './watch/FsEvent' ;
2425
2526const pathSep = posix ? posix . sep : sep ;
2627const pathRelative = posix ? posix . relative : relative ;
@@ -101,6 +102,12 @@ export class Superblock {
101102 this . root = root ;
102103 }
103104
105+ public onchange ?: ( event : FsEvent ) => void ;
106+
107+ protected emit ( change : FsEvent ) {
108+ if ( this . onchange ) this . onchange ( change ) ;
109+ }
110+
104111 createLink ( ) : Link ;
105112 createLink ( parent : Link , name : string , isDirectory ?: boolean , mode ?: number ) : Link ;
106113 createLink ( parent ?: Link , name ?: string , isDirectory : boolean = false , mode ?: number ) : Link {
@@ -132,6 +139,17 @@ export class Superblock {
132139 return false ;
133140 }
134141
142+ private _emitDeleteRecursive ( link : Link ) {
143+ if ( link . getNode ( ) . isDirectory ( ) ) {
144+ for ( const [ name , child ] of link . children . entries ( ) ) {
145+ if ( child && name !== '.' && name !== '..' ) {
146+ this . _emitDeleteRecursive ( child ) ;
147+ }
148+ }
149+ }
150+ this . emit ( new FsEvent ( FsEventType . DELETE , link . steps , link . getNode ( ) ) ) ;
151+ }
152+
135153 private newInoNumber ( ) : number {
136154 const releasedFd = this . releasedInos . pop ( ) ;
137155
@@ -489,7 +507,10 @@ export class Superblock {
489507 this . fds [ file . fd ] = file ;
490508 this . openFiles ++ ;
491509
492- if ( flagsNum & O_TRUNC ) file . truncate ( ) ;
510+ if ( flagsNum & O_TRUNC ) {
511+ file . truncate ( ) ;
512+ this . emit ( new FsEvent ( FsEventType . MODIFY , file . link . steps , file . node ) ) ;
513+ }
493514
494515 return file ;
495516 }
@@ -527,6 +548,7 @@ export class Superblock {
527548 modeNum ??= 0o666 ;
528549
529550 link = this . createLink ( dirLink , steps [ steps . length - 1 ] , false , modeNum ) ;
551+ this . emit ( new FsEvent ( FsEventType . CREATE , link . steps , link . getNode ( ) ) ) ;
530552 } else throw err ;
531553 }
532554
@@ -622,13 +644,15 @@ export class Superblock {
622644 if ( dir2 . getChild ( name ) ) throw createError ( ERROR_CODE . EEXIST , 'link' , filename1 , filename2 ) ;
623645 const node = link1 . getNode ( ) ;
624646 node . nlink ++ ;
625- dir2 . createChild ( name , node ) ;
647+ const newLink = dir2 . createChild ( name , node ) ;
648+ this . emit ( new FsEvent ( FsEventType . CREATE , newLink . steps , node ) ) ;
626649 } ;
627650
628651 public readonly unlink = ( filename : string ) => {
629652 const link : Link = this . getLinkOrThrow ( filename , 'unlink' ) ;
630653 // TODO: Check if it is file, dir, other...
631654 if ( link . length ) throw Error ( 'Dir not empty...' ) ;
655+ this . _emitDeleteRecursive ( link ) ;
632656 this . deleteLink ( link ) ;
633657 const node = link . getNode ( ) ;
634658 node . nlink -- ;
@@ -661,6 +685,7 @@ export class Superblock {
661685 // Create symlink.
662686 const symlink : Link = dirLink . createChild ( name ) ;
663687 symlink . getNode ( ) . makeSymlink ( targetFilename ) ;
688+ this . emit ( new FsEvent ( FsEventType . CREATE , symlink . steps , symlink . getNode ( ) ) ) ;
664689 return symlink ;
665690 } ;
666691
@@ -709,9 +734,11 @@ export class Superblock {
709734
710735 // Rename should overwrite the new path, if that exists.
711736 const name = basename ( newPathFilename ) ;
737+ const oldSteps = link . steps ;
712738 link . name = name ;
713739 link . steps = [ ...newPathDirLink . steps , name ] ;
714740 newPathDirLink . setChild ( link . getName ( ) , link ) ;
741+ this . emit ( new FsEvent ( FsEventType . MOVE , link . steps , link . getNode ( ) , oldSteps ) ) ;
715742 } ;
716743
717744 public readonly mkdir = ( filename : string , modeNum : number ) : void => {
@@ -724,7 +751,8 @@ export class Superblock {
724751 if ( dir . getChild ( name ) ) throw createError ( ERROR_CODE . EEXIST , 'mkdir' , filename ) ;
725752 const node = dir . getNode ( ) ;
726753 if ( ! node . canWrite ( ) || ! node . canExecute ( ) ) throw createError ( ERROR_CODE . EACCES , 'mkdir' , filename ) ;
727- dir . createChild ( name , this . createNode ( constants . S_IFDIR | modeNum ) ) ;
754+ const child = dir . createChild ( name , this . createNode ( constants . S_IFDIR | modeNum ) ) ;
755+ this . emit ( new FsEvent ( FsEventType . CREATE , child . steps , child . getNode ( ) ) ) ;
728756 } ;
729757
730758 /**
@@ -759,13 +787,15 @@ export class Superblock {
759787 }
760788 created = true ;
761789 curr = curr . createChild ( steps [ i ] , this . createNode ( constants . S_IFDIR | modeNum ) ) ;
790+ this . emit ( new FsEvent ( FsEventType . CREATE , curr . steps , curr . getNode ( ) ) ) ;
762791 }
763792 return created ? filename : undefined ;
764793 } ;
765794
766795 public readonly rmdir = ( filename : string , recursive : boolean = false ) => {
767796 const link = this . getLinkAsDirOrThrow ( filename , 'rmdir' ) ;
768797 if ( link . length && ! recursive ) throw createError ( ERROR_CODE . ENOTEMPTY , 'rmdir' , filename ) ;
798+ this . _emitDeleteRecursive ( link ) ;
769799 this . deleteLink ( link ) ;
770800 } ;
771801
@@ -781,6 +811,7 @@ export class Superblock {
781811 }
782812 if ( link . getNode ( ) . isDirectory ( ) && ! recursive ) throw createError ( ERROR_CODE . ERR_FS_EISDIR , 'rm' , filename ) ;
783813 if ( ! link . parent ?. getNode ( ) . canWrite ( ) ) throw createError ( ERROR_CODE . EACCES , 'rm' , filename ) ;
814+ this . _emitDeleteRecursive ( link ) ;
784815 this . deleteLink ( link ) ;
785816 } ;
786817
@@ -802,6 +833,73 @@ export class Superblock {
802833 if ( file . node . isSymlink ( ) ) {
803834 throw createError ( ERROR_CODE . EBADF , 'write' , file . link . getPath ( ) ) ;
804835 }
805- return file . write ( buf , offset , length , position === - 1 || typeof position !== 'number' ? undefined : position ) ;
836+ const bytes = file . write (
837+ buf ,
838+ offset ,
839+ length ,
840+ position === - 1 || typeof position !== 'number' ? undefined : position ,
841+ ) ;
842+ if ( bytes > 0 ) this . emit ( new FsEvent ( FsEventType . MODIFY , file . link . steps , file . node ) ) ;
843+ return bytes ;
806844 }
845+
846+ public readonly ftruncate = ( fd : number , len ?: number ) : void => {
847+ const file = this . getFileByFdOrThrow ( fd , 'ftruncate' ) ;
848+ file . truncate ( len ) ;
849+ this . emit ( new FsEvent ( FsEventType . MODIFY , file . link . steps , file . node ) ) ;
850+ } ;
851+
852+ public readonly fchmod = ( fd : number , modeNum : number ) : void => {
853+ const file = this . getFileByFdOrThrow ( fd , 'fchmod' ) ;
854+ file . chmod ( modeNum ) ;
855+ this . emit ( new FsEvent ( FsEventType . MODIFY , file . link . steps , file . node ) ) ;
856+ } ;
857+
858+ public readonly chmod = ( filename : string , modeNum : number ) : void => {
859+ const link = this . getResolvedLinkOrThrow ( filename , 'chmod' ) ;
860+ link . getNode ( ) . chmod ( modeNum ) ;
861+ this . emit ( new FsEvent ( FsEventType . MODIFY , link . steps , link . getNode ( ) ) ) ;
862+ } ;
863+
864+ public readonly lchmod = ( filename : string , modeNum : number ) : void => {
865+ const link = this . getLinkOrThrow ( filename , 'lchmod' ) ;
866+ link . getNode ( ) . chmod ( modeNum ) ;
867+ this . emit ( new FsEvent ( FsEventType . MODIFY , link . steps , link . getNode ( ) ) ) ;
868+ } ;
869+
870+ public readonly fchown = ( fd : number , uid : number , gid : number ) : void => {
871+ const file = this . getFileByFdOrThrow ( fd , 'fchown' ) ;
872+ file . chown ( uid , gid ) ;
873+ this . emit ( new FsEvent ( FsEventType . MODIFY , file . link . steps , file . node ) ) ;
874+ } ;
875+
876+ public readonly chown = ( filename : string , uid : number , gid : number ) : void => {
877+ const link = this . getResolvedLinkOrThrow ( filename , 'chown' ) ;
878+ link . getNode ( ) . chown ( uid , gid ) ;
879+ this . emit ( new FsEvent ( FsEventType . MODIFY , link . steps , link . getNode ( ) ) ) ;
880+ } ;
881+
882+ public readonly lchown = ( filename : string , uid : number , gid : number ) : void => {
883+ const link = this . getLinkOrThrow ( filename , 'lchown' ) ;
884+ link . getNode ( ) . chown ( uid , gid ) ;
885+ this . emit ( new FsEvent ( FsEventType . MODIFY , link . steps , link . getNode ( ) ) ) ;
886+ } ;
887+
888+ public readonly futimes = ( fd : number , atime : number , mtime : number ) : void => {
889+ const file = this . getFileByFdOrThrow ( fd , 'futimes' ) ;
890+ const node = file . node ;
891+ node . atime = new Date ( atime * 1000 ) ;
892+ node . mtime = new Date ( mtime * 1000 ) ;
893+ this . emit ( new FsEvent ( FsEventType . MODIFY , file . link . steps , file . node ) ) ;
894+ } ;
895+
896+ public readonly utimes = ( filename : string , atime : number , mtime : number , followSymlinks : boolean = true ) : void => {
897+ const link = followSymlinks
898+ ? this . getResolvedLinkOrThrow ( filename , 'utimes' )
899+ : this . getLinkOrThrow ( filename , 'lutimes' ) ;
900+ const node = link . getNode ( ) ;
901+ node . atime = new Date ( atime * 1000 ) ;
902+ node . mtime = new Date ( mtime * 1000 ) ;
903+ this . emit ( new FsEvent ( FsEventType . MODIFY , link . steps , node ) ) ;
904+ } ;
807905}
0 commit comments