{ This file is part of the Pas2JS run time library. Copyright (c) 2018 by Mattias Gaertner See the file COPYING.FPC, included in this distribution, for details about the copyright. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. **********************************************************************} unit node.fs; {$mode objfpc} {$ModeSwitch externalclass} interface uses JS, NodeJS, Types, SysUtils; var DirectorySeparator: char = '/'; DriveSeparator: string = ''; ExtensionSeparator: char = '.'; PathSeparator: char = ':'; AllowDirectorySeparators: set of char = ['\','/']; AllowDriveSeparators: set of char = []; AllDirectorySeparators: set of char = ['\','/']; AllFilesMask: string = '*'; MaxPathLen: integer = 4096; PathDelim: char = '/'; // FPC = DirectorySeparator DriveDelim: string = ''; // FPC = DriveSeparator PathSep: char = ':';// FPC = PathSeparator MAX_PATH: integer = 4096; // FPC = MaxPathLen; const //faReadOnly = 1; //faHidden = 2; //faSystem = 4; //faReserve = 8; faDirectory = 16; //faArchive = 32; faAnyFile = $000001FF; function GetCurrentDir: string; function FileExists(const Filename: string): boolean; function DirectoryExists(const Filename: string): boolean; function ExtractFilePath(const Filename: string): string; function ExtractFileName(const Filename: string): string; function ExtractFileExt(const Filename: string): string; function SetDirSeparators(const Filename: string): string; function ExpandFileName(const Filename: string): string; function ExcludeTrailingPathDelimiter(const Filename: string): string; function IncludeTrailingPathDelimiter(const Filename: string): string; function ChangeFileExt(const Filename, NewExt: string): string; function DeleteFile(const Filename: String): Boolean; function RenameFile(const OldName, NewName: String): Boolean; type TSearchRec = record Time : Longint; Size : nativeint; Attr : Longint; Name : String; ExcludeAttr : Longint; FindHandle : Pointer; //Mode : TMode; //FindData : TFindData; end; TUnicodeSearchRec = TSearchRec; function FindFirst(const Path: String; Attr : Longint; out Rslt: TSearchRec): Longint; function FindNext(var Rslt: TSearchRec): Longint; procedure FindClose(var F: TSearchrec); function FileDateToDateTime(Filedate : Longint): TDateTime; function DateTimeToFileDate(DateTime: TDateTime): longint; const S_IRUSR = &400; // read by owner S_IWUSR = &200; // write by owner S_IXUSR = &100; // execute/search by owner S_IRGRP = &40; // read by group S_IWGRP = &20; // write by group S_IXGRP = &10; // execute/search by group S_IROTH = &4; // read by others S_IWOTH = &2; // write by others S_IXOTH = &1; // execute/search by others F_OK: nativeint; external name 'fs.constants.F_OK'; // file visible R_OK: nativeint; external name 'fs.constants.R_OK'; // file readable W_OK: nativeint; external name 'fs.constants.W_OK'; // file writable X_OK: nativeint; external name 'fs.constants.X_OK'; // file executable COPYFILE_EXCL: nativeint; external name 'fs.constants.COPYFILE_EXCL'; COPYFILE_FICLONE: NativeInt; external name 'fs.constants.COPYFILE_FICLONE'; COPYFILE_FICLONE_FORCE: NativeInt; external name 'fs.constants.COPYFILE_FICLONE_FORCE'; type TNJSFileDesc = JSValue; // integer or nil TNJSFileMode = NativeInt; { TNJSDirEnt } TNJSDirEnt = class external name 'fs.Dirent' private FName: string; external name 'name'; public function isBlockDevice: boolean; function isCharacterDevice: boolean; function isDirectory: boolean; function isFIFO: boolean; function isFile: boolean; function isSocket: boolean; function isSymbolicLink: boolean; property Name: string read FName; end; TNJSDirEntArray = array of TNJSDirEnt; { TNJSStats } TNJSStats = class external name 'fs.Stats' private public dev: NativeInt; ino: NativeInt; mode: TNJSFileMode; nlink: NativeInt; uid: NativeInt; gid: NativeInt; rdev: NativeInt; size: NativeInt; blksize: NativeInt; blocks: NativeInt; atimeMs: Double; mtimeMs: Double; ctimeMs: Double; birthtimeMs: Double; atime: TJSDate; mtime: TJSDate; ctime: TJSDate; birthtime: TJSDate; function isBlockDevice: boolean; function isCharacterDevice: boolean; function isDirectory: boolean; function isFIFO: boolean; function isFile: boolean; function isSocket: boolean; function isSymbolicLink: boolean; end; { TNJSStreamReadable } TNJSStreamReadable = class external name 'stream.Readable' private FReadableHighWaterMark: NativeInt external name 'readableHighWaterMark'; FReadableLength: NativeInt external name 'readableLength'; public function destroy(Error: TJSError): TNJSStreamReadable; function isPaused: boolean; function pause: TNJSStreamReadable; // pipe(destination[, options]) function read: jsvalue; // string | buffer | nil | any function read(size: nativeint): jsvalue; // string | buffer | nil | any property ReadableHighWaterMark: NativeInt read FReadableHighWaterMark; property ReadableLength: NativeInt read FReadableLength; function resume: TNJSStreamReadable; function setEncoding(Encoding: string): TNJSStreamReadable; // unpipe([destination]) // unshift(chunk) // wrap(stream) end; { TNJSReadStream } TNJSReadStream = class external name 'fs.ReadStream'(TNJSStreamReadable) private fBytesRead: NativeInt external name 'bytesRead'; FPath: string external name 'path'; public property BytesRead: NativeInt read fBytesRead; property Path: string read FPath; end; TNJSStreamWritableEndHandler = reference to procedure; { TNJSStreamWritable } TNJSStreamWritable = class external name 'stream.Writable' private FwritableHighWaterMark: nativeint external name 'writableHighWaterMark'; FwritableLength: nativeint external name 'writableLength'; public procedure cork; function destroy(Error: TJSError): TNJSStreamWritable; function _end(chunk: string): TNJSStreamWritable; external name 'end'; function _end(chunk: string; encoding: string; callback: TNJSStreamWritableEndHandler = nil): TNJSStreamWritable; external name 'end'; function setDefaultEncoding(Encoding: string): TNJSStreamWritable; procedure uncork; property writableHighWaterMark: NativeInt read FwritableHighWaterMark; property writableLength: nativeint read FwritableLength; function write(chunk: string): boolean; function write(chunk: string; encoding: string; callback: TNJSStreamWritableEndHandler): boolean; end; { TNJSWriteStream } TNJSWriteStream = class external name 'fs.WriteStream'(TNJSStreamWritable) private FBytesWritten: NativeInt external name 'bytesWritten'; FPath: string external name 'path'; public property BytesWritten: NativeInt read FBytesWritten; property Path: string read FPath; end; { TNJSAppendFileOpts } TNJSAppendFileOpts = record Encoding: string external name 'encoding'; // default nil Mode: TNJSFileMode external name 'mode'; // default &666 Flag: string external name 'flag'; // default 'a' end; { TNJSReadStreamOpts } TNJSReadStreamOpts = record Flags: string external name 'flags'; // default 'r' Encoding: string external name 'encoding'; // default nil FD: TNJSFileDesc external name 'fd'; // default nil Mode: TNJSFileMode external name 'mode'; // default &666 AutoClose: boolean external name 'autoclose'; // default true StartByte: NativeInt external name 'start'; EndByte: NativeInt external name 'end'; // default Infinity HighWaterMark: NativeInt external name 'highWaterMark'; // default 64*1024 end; { TNJSWriteStreamOpts } TNJSWriteStreamOpts = record Flags: string external name 'flags'; // default 'w' Encoding: string external name 'encoding'; // default 'utf8' FD: TNJSFileDesc external name 'fd'; // default nil Mode: TNJSFileMode external name 'mode'; // default &666 AutoClose: boolean external name 'autoclose'; // default true Start: NativeInt external name 'start'; end; { TNJSStatOpts } TNJSStatOpts = record bigint: boolean; end; { TNJSMkdirOpts } TNJSMkdirOpts = record recursive: boolean; // default false mode: nativeint; // default &777 end; { TNJSReadDirOpts } TNJSReadDirOpts = record encoding: string; // default 'utf8' withFileTypes: boolean; // default false end; { TNJSReadFileOpts } TNJSReadFileOpts = record encoding: string; // default nil flag: string; // default 'r' end; { TNJSReadLinkOpts } TNJSReadLinkOpts = record encoding: string; // default 'utf8' end; TNJSWriteFileOpts = record encoding: string; // default 'utf8' mode: nativeint; // default &666 flag: string; // default 'w' end; type { TNJSFS - nodejs filesystem } TNJSFS = class external name 'fs' public procedure accessSync(Path: string; Mode: TNJSFileMode = F_OK); // throws Error if access is not granted procedure appendFileSync(Path: string; Data: string); procedure appendFileSync(Path: string; Data: string; const Options: TJSObject{TNJSAppendFileOpts}); procedure chmodSync(Path: string; Mode: TNJSFileMode); procedure chownSync(Path: string; uid, gid: NativeInt); procedure closeSync(fd: TNJSFileDesc); procedure copyFileSync(Src, Dest: string; Flags: NativeInt = 0 // see COPYFILE_EXCL etc ); function createReadStream(Path: string): TNJSWriteStream; function createReadStream(Path: string; const Options: TJSObject{TNJSReadStreamOpts}): TNJSReadStream; function createWriteStream(Path: string): TNJSWriteStream; function createWriteStream(Path: string; const Options: TJSObject{TNJSWriteStreamOpts}): TNJSWriteStream; function existsSync(Path: string): boolean; procedure fchmodSync(fd: TNJSFileDesc; mode: TNJSFileMode); procedure fchownSync(fd: TNJSFileDesc; uid, gid: NativeInt); procedure fdatasyncSync(fd: TNJSFileDesc); procedure fstatSync(fd: TNJSFileDesc; const Options: TJSObject{TNJSStatOpts}); procedure fsyncSync(fd: TNJSFileDesc); procedure ftruncateSync(fd: TNJSFileDesc; Len: nativeint = 0); procedure futimesSync(fd: TNJSFileDesc; atime: NativeInt; mtime: NativeInt); procedure lchownSync(path: string; uid, gid: NativeInt); procedure linkSync(ExistingPath, NewPath: string); procedure lstatSync(Path: string); procedure lstatSync(Path: string; const Options: TJSObject{TNJSStatOpts}); procedure mkdirSync(Path: string; const Options: TJSObject{TNJSMkdirOpts}); // mkdtempSync function openSync(Path: string; Flags: string; mode: TNJSFileMode): TNJSFileDesc; function readdirSync(Path: string): TJSArray; // TStringDynArray function readdirSync(Path: string; const Options: TJSObject{TNJSReadDirOpts}): TJSArray; // can be TStringDynArray or TNJSDirEntArray if withFileTypes=true function readFileSync(Path: string; const Options: TJSObject{TNJSReadFileOpts}): string; function readlinkSync(Path: string): string; function readlinkSync(Path: string; const Options: TJSObject{TNJSReadLinkOpts}): string; // readSync(fd, buffer, offset, length, position) // realpathSync(path[, options]) procedure renameSync(OldPath, NewPath: string); procedure rmdirSync(Path: string); function statSync(Path: string): TNJSStats; function statSync(Path: string; const Options: TJSObject{TNJSStatOpts}): TNJSStats; procedure symlinkSync(Target, Path: string; LinkType: string = 'file'); procedure truncateSync(Path: string; len: NativeInt = 0); procedure unlinkSync(Path: string); // unwatchFile(filename[, listener]) // utimesSync(path, atime, mtime) // watch(filename[, options][, listener]) // watchFile(filename[, options], listener) procedure writeFileSync( aFile: jsvalue; // string | buffer | URL | filedescriptor Data: jsvalue // string | buffer | typedarray | DataView ); procedure writeFileSync( aFile: jsvalue; // string | buffer | URL | filedescriptor Data: jsvalue; // string | buffer | typedarray | DataView const Options: TJSObject{TNJSWriteFileOpts}); function writeSync(fd: TNJSFileDesc; buffer: jsvalue; // buffer | TypedArray | DataView Offset, Count, Position: NativeInt): NativeInt; function writeSync(fd: TNJSFileDesc; Data: string; Position: NativeInt; Encoding: string): NativeInt; end; type { TNJSPathParsed } TNJSPathParsed = class external name 'TNJSPathParsed' public dir: string; root: string; base: string; name: string; ext: string; end; { TNJSPath } TNJSPath = class external name 'path' public win32: TJSObject; // todo posix: TJSObject; // todo public const // Beware: nodejs uses "separator" and "delimiter" the other way round than FPC/Delphi delimiter: char; // search PATH delimiter, windows ;, posix : sep: char; // directory delimiter, windows \, posix: / public function basename(Path: string; Ext: string = ''): string; // remove the directory, optional ext to chomp off function dirname(Path: string): string; // returns directory without trailing sep function extname(Path: string): string; // returns from last occurence of '.', if path starts with '.' the empty string is returned function format(PathObject: TJSObject): string; { PathObjectis can contain the following string properties: dir, root, base, name, ext root is ignored if dir exists ext and name are ignored if base exists } function isAbsolute(Path: string): boolean; // '' returns false function join(Path1: string): string; varargs; // joins all passed strings with sep and normalizes, e.g. resolves '..', if the result is empty it returns '.' function normalize(Path: string): string; // resolves '..' and '.' folders, reduces multiple delimiters, trailing sep is preserved, empty string is returned as '.', on windows replaces / with \\ function parse(Path: string): TNJSPathParsed; function relative(FromPath, ToPath: string): string; // resolve both, then create relative, if both the same returns '' function resolve(Path1: string): string; varargs; // resolve from right to left, prepend until an absolute path is created function toNamespacedPath(Path: string): string; // windows only end; var NJS_FS: TNJSFS; NJS_Path: TNJSPath; implementation function GetCurrentDir: string; begin Result:=NJS_Path.resolve(''); end; function FileExists(const Filename: string): boolean; begin Result:=NJS_FS.existsSync(Filename); end; function DirectoryExists(const Filename: string): boolean; var stats: TNJSStats; begin try stats:=NJS_FS.statSync(Filename); except exit(false); end; Result:=stats.isDirectory; end; function ExtractFilePath(const Filename: string): string; var i : longint; begin i := Length(FileName); while (i > 0) and not (FileName[i] in AllDirectorySeparators) do Dec(i); If I>0 then Result := Copy(FileName, 1, i) else Result:=''; end; function ExtractFileName(const Filename: string): string; var i : longint; begin I := Length(FileName); while (I > 0) and not (FileName[i] in AllDirectorySeparators) do Dec(I); Result := Copy(FileName, I + 1, MaxInt); end; function ExtractFileExt(const Filename: string): string; var i : longint; SOF : Boolean; // Dot at Start of filename ? begin Result:=''; I := Length(FileName); while (I > 0) and not (FileName[i] in AllDirectorySeparators) do begin if (Filename[i]=ExtensionSeparator) then begin SOF:=(I=1) or (FileName[i-1] in AllowDirectorySeparators); if (Not SOF) or FirstDotAtFileNameStartIsExtension then Result:=Copy(FileName, I, MaxInt); exit; end; Dec(I); end; end; function SetDirSeparators(const Filename: string): string; var i: Integer; begin Result:=Filename; For i:=1 to Length(Result) do If (Result[i] in AllowDirectorySeparators) and (Result[i]<>DirectorySeparator) then Result[i]:=DirectorySeparator; end; function ExpandFileName(const Filename: string): string; var IsAbs: Boolean; HomeDir, Fn: String; begin Fn := SetDirSeparators(Filename); IsAbs := NJS_Path.isAbsolute(Fn); if (not IsAbs) then begin if (PathDelim='/') and (((Length(Fn) > 1) and (Fn[1] = '~') and (Fn[2] = '/')) or (Fn = '~') ) then begin HomeDir := NJS_OS.homedir; if not NJS_Path.isAbsolute(HomeDir) then HomeDir := ExpandFileName(HomeDir); Fn := HomeDir + Copy(Fn,2,length(Fn)); IsAbs := True; end; end; if IsAbs then begin Result := NJS_Path.resolve(Fn); end else begin Fn := IncludeTrailingPathDelimiter(GetCurrentDir) + Fn; Fn := NJS_Path.resolve(Fn); Result := Fn; end; end; function ExcludeTrailingPathDelimiter(const Filename: string): string; Var L : Integer; begin L:=Length(Filename); If (L>0) and (Filename[L] in AllowDirectorySeparators) then Result:=LeftStr(Filename,L-1) else Result:=Filename; end; function IncludeTrailingPathDelimiter(const Filename: string): string; Var l : Integer; begin Result:=Filename; l:=Length(Result); If (L=0) or not (Result[l] in AllowDirectorySeparators) then Result+=DirectorySeparator; end; function ChangeFileExt(const Filename, NewExt: string): string; var i : longint; SOF : Boolean; // start of filename begin Result:=Filename; i := Length(Result); for i:=length(Result) downto 1 do if Result[i] in AllDirectorySeparators then break else if (Result[i]=ExtensionSeparator) then begin SOF:=(I=1) or (Result[i-1] in AllowDirectorySeparators); if (Not SOF) or FirstDotAtFileNameStartIsExtension then begin Result := LeftStr(Result, I - 1) + NewExt; exit; end; end; Result+=NewExt; end; function DeleteFile(const Filename: String): Boolean; begin try NJS_FS.unlinkSync(Filename); except exit(false); end; Result:=true; end; function RenameFile(const OldName, NewName: String): Boolean; begin try NJS_FS.renameSync(OldName,NewName); except exit(false); end; Result:=true; end; function FindFirst(const Path: String; Attr: Longint; out Rslt: TSearchRec ): Longint; var Mask: String; Entries: TStringDynArray; Iterator: TJSObject; begin Mask:=ExtractFileName(Path); if Mask<>AllFilesMask then raise Exception.Create('FindFirst: ToDo: Mask='+Path); try Entries:=TStringDynArray(NJS_FS.readdirSync(NJS_Path.dirname(Path))); except exit(-1); end; Iterator:=TJSObject.new; Iterator['path']:=ExtractFilePath(Path); Iterator['index']:=-1; Iterator['entries']:=Entries; Iterator['attr']:=Attr; Rslt.FindHandle:=Iterator; Result:=FindNext(Rslt); end; function FindNext(var Rslt: TSearchRec): Longint; var Iterator: TJSObject; Entries: TStringDynArray; Index: NativeInt; Attr: LongInt; Path: String; Stats: TNJSStats; Name: string; IsDirectory: Boolean; begin Iterator:=TJSObject(Rslt.FindHandle); Path:=IncludeTrailingPathDelimiter(String(Iterator['path'])); Entries:=TStringDynArray(Iterator['entries']); Attr:=longint(Iterator['attr']); Index:=NativeInt(Iterator['index']); //writeln('FindNext Path=',Path,' Index=',Index,'/',length(Entries),' Attr=',Attr); repeat inc(Index); if Index>=length(Entries) then break; Name:=Entries[Index]; Rslt.Name:=Name; Stats:=nil; try Stats:=NJS_FS.statSync(Path+Name); except end; if Stats=nil then continue; IsDirectory:=Stats.isDirectory; if IsDirectory and (faDirectory and Attr=0) then continue; // fill in Rslt Rslt.Time:=Stats.mtime.time div 1000; Rslt.Size:=Stats.size; Rslt.Attr:=0; if IsDirectory then Rslt.Attr+=faDirectory; Iterator['index']:=Index; exit(0); until false; Iterator['index']:=length(Entries); Result:=-1; end; procedure FindClose(var F: TSearchrec); begin F.FindHandle:=nil; end; function FileDateToDateTime(Filedate: Longint): TDateTime; var d: TJSDate; begin d:=TJSDate.new(Filedate*1000); Result:=JSDateToDateTime(d); end; function DateTimeToFileDate(DateTime: TDateTime): longint; var d: TJSDate; begin d:=DateTimeToJSDate(DateTime); Result:=d.Time div 1000; end; initialization NJS_FS:=TNJSFS(Require('fs')); NJS_Path:=TNJSPath(Require('path')); PathDelim:=NJS_Path.sep; PathSeparator:=NJS_Path.delimiter; DirectorySeparator:=NJS_Path.sep; PathSep:=NJS_Path.delimiter; case lowercase(NJS_OS.platform) of 'win32': begin DriveSeparator:=':'; AllowDriveSeparators:=[':']; MaxPathLen:=260; MAX_PATH:=MaxPathLen; end; end; AllDirectorySeparators:=AllowDirectorySeparators+AllowDriveSeparators; end.