diff --git a/src/fs/generateCompileProgram.ts b/src/fs/generateCompileProgram.ts new file mode 100644 index 0000000..8e63926 --- /dev/null +++ b/src/fs/generateCompileProgram.ts @@ -0,0 +1,58 @@ +import path from 'path' +import { isFolder, listFilesInFolder, listSubFoldersInFolder } from '../file' +import { + getInitialCode, + getCompiledMacrosCode, + generateCodeForFileCreation +} from './internal/helper' +import { HashedFolder } from '../types' + +export const generateCompileProgram = async (folderPath: string) => { + const compiledMacrosCode = await getCompiledMacrosCode(['mf_mkdir.sas']) + + const initialProgramContent = getInitialCode() + + const folderCreationCode = await fileAndDirectoryCreationCode(folderPath) + + return compiledMacrosCode + initialProgramContent + folderCreationCode +} + +const fileAndDirectoryCreationCode = async ( + resourcePath: string, + pathRelativeTo: string = resourcePath, + resultCode: string = '' +) => { + if (!(await isFolder(resourcePath))) { + resultCode += await generateCodeForFileCreation( + resourcePath, + pathRelativeTo + ) + return resultCode + } + + const files = await listFilesInFolder(resourcePath) + for (const file of files) { + resultCode = await fileAndDirectoryCreationCode( + path.join(resourcePath, file), + pathRelativeTo, + resultCode + ) + } + + const subFolders = await listSubFoldersInFolder(resourcePath) + for (const folder of subFolders) { + const folderPath = path.join(resourcePath, folder) + + resultCode = `${resultCode} +%mf_mkdir(&fsTarget${folderPath.replace(pathRelativeTo, '')}) +` + + resultCode = await fileAndDirectoryCreationCode( + folderPath, + pathRelativeTo, + resultCode + ) + } + + return resultCode +} diff --git a/src/fs/internal/helper.ts b/src/fs/internal/helper.ts new file mode 100644 index 0000000..4ee2b7a --- /dev/null +++ b/src/fs/internal/helper.ts @@ -0,0 +1,87 @@ +import path from 'path' +import { getNodeModulePath } from '../../utils/getNodeModulePath' +import { chunk } from '../../utils/chunk' +import { readFile, base64EncodeFile } from '../../file' + +export const generateCodeForFileCreation = async ( + filePath: string, + pathRelativeTo: string +) => { + const base64EncodedFileContent = await base64EncodeFile(filePath) + const chunkedFileContent = chunkFileContent(base64EncodedFileContent) + return ` +filename _in64 temp lrecl=99999999; +data _null_; +file _in64; +${chunkedFileContent} +run; + +filename _out64 "&fsTarget${filePath.replace(pathRelativeTo, '')}"; + +/* convert from base64 */ +data _null_; +length filein 8 fileout 8; +filein = fopen("_in64",'I',4,'B'); +fileout = fopen("_out64",'O',3,'B'); +char= '20'x; +do while(fread(filein)=0); + length raw $4 ; + do i=1 to 4; + rc=fget(filein,char,1); + substr(raw,i,1)=char; + end; + rc = fput(fileout, input(raw,$base64X4.)); + rc =fwrite(fileout); +end; +rc = fclose(filein); +rc = fclose(fileout); +run; + +filename _in64 clear; +filename _out64 clear; +` +} + +export const chunkFileContent = (fileContent: string) => { + const chunkedLines = chunk(fileContent) + + if (chunkedLines.length === 1) { + return ` put '${chunkedLines[0].split("'").join("''")}';\n` + } + + let combinedLines = '' + + chunkedLines.forEach((chunkedLine, index) => { + const text = ` put '${chunkedLine.split("'").join("''")}'${ + index !== chunkedLines.length - 1 ? '@;\n' : ';\n' + }` + + combinedLines += text + }) + + return combinedLines +} + +export const getInitialCode = () => `%global fsTarget; +%let compiled_fsTarget=%sysfunc(pathname(work)); +%let fsTarget=%sysfunc(coalescec(&fsTarget,&compiled_fsTarget)); +options nobomfile; + +%mf_mkdir(&fsTarget) +` + +export const getCompiledMacrosCode = async (macros: string[]) => { + const sasjsCorePath = await getNodeModulePath('@sasjs/core') + if (!sasjsCorePath) throw new Error('@sasjs/core could not be found') + + let compiledCode = '' + + for (const macro of macros) { + const macroPath = path.join(sasjsCorePath, 'base', macro) + const macroContent = await readFile(macroPath) + + compiledCode += macroContent + '\n' + } + + return compiledCode +} diff --git a/src/fs/spec/createFSCompileProgram.spec.ts b/src/fs/spec/createFSCompileProgram.spec.ts new file mode 100644 index 0000000..e629dab --- /dev/null +++ b/src/fs/spec/createFSCompileProgram.spec.ts @@ -0,0 +1,29 @@ +import path from 'path' +import { generateCompileProgram } from '../generateCompileProgram' +import { createFile, createFolder, deleteFolder, readFile } from '../../file' + +describe('createFSCompileProgram', () => { + const timestamp = new Date().valueOf() + const folderName = `test-create-folder-${timestamp}` + const folderPath = path.join(__dirname, folderName) + const subFolderPath = path.join(folderPath, 'subFolder') + const filePath = path.join(subFolderPath, 'file.txt') + + beforeAll(async () => { + await createFolder(folderPath) + await createFolder(subFolderPath) + await createFile(filePath, 'this is dummy file content') + }) + + afterAll(async () => { + await deleteFolder(folderPath) + }) + + it('should return a sas program ', async () => { + const program = await generateCompileProgram(folderPath) + const expectedProgram = await readFile( + path.join(__dirname, 'files', 'compiledProgram.sas') + ) + expect(program).toEqual(expectedProgram) + }) +}) diff --git a/src/fs/spec/files/compiledProgram.sas b/src/fs/spec/files/compiledProgram.sas new file mode 100644 index 0000000..0aa8a81 --- /dev/null +++ b/src/fs/spec/files/compiledProgram.sas @@ -0,0 +1,112 @@ +%global fsTarget; +%let compiled_fsTarget=%sysfunc(pathname(work)); +%let fsTarget=%sysfunc(coalescec(&fsTarget,&compiled_fsTarget)); +options nobomfile; + +/* mf_mkdir macro begins */ +/** + @file + @brief Creates a directory, including any intermediate directories + @details Works on windows and unix environments via dcreate function. +Usage: + + %mf_mkdir(/some/path/name) + + + @param dir relative or absolute pathname. Unquoted. + @version 9.2 + +**/ + +%macro mf_mkdir(dir +)/*/STORE SOURCE*/; + + %local lastchar child parent; + + %let lastchar = %substr(&dir, %length(&dir)); + %if (%bquote(&lastchar) eq %str(:)) %then %do; + /* Cannot create drive mappings */ + %return; + %end; + + %if (%bquote(&lastchar)=%str(/)) or (%bquote(&lastchar)=%str(\)) %then %do; + /* last char is a slash */ + %if (%length(&dir) eq 1) %then %do; + /* one single slash - root location is assumed to exist */ + %return; + %end; + %else %do; + /* strip last slash */ + %let dir = %substr(&dir, 1, %length(&dir)-1); + %end; + %end; + + %if (%sysfunc(fileexist(%bquote(&dir))) = 0) %then %do; + /* directory does not exist so prepare to create */ + /* first get the childmost directory */ + %let child = %scan(&dir, -1, %str(/\:)); + + /* + If child name = path name then there are no parents to create. Else + they must be recursively scanned. + */ + + %if (%length(&dir) gt %length(&child)) %then %do; + %let parent = %substr(&dir, 1, %length(&dir)-%length(&child)); + %mf_mkdir(&parent) + %end; + + /* + Now create the directory. Complain loudly of any errs. + */ + + %let dname = %sysfunc(dcreate(&child, &parent)); + %if (%bquote(&dname) eq ) %then %do; + %put %str(ERR)OR: could not create &parent + &child; + %abort cancel; + %end; + %else %do; + %put Directory created: &dir; + %end; + %end; + /* exit quietly if directory did exist.*/ +%mend mf_mkdir; + +/* mf_mkdir macro ends */ + +%mf_mkdir(&fsTarget) + + +%mf_mkdir(&fsTarget/subFolder) + +filename _in64 temp lrecl=99999999; +data _null_; +file _in64; + put 'dGhpcyBpcyBkdW1teSBmaWxlIGNvbnRlbnQ='; + +run; + +filename _out64 "&fsTarget/subFolder/file.txt"; + +/* convert from base64 */ +data _null_; + length filein 8 fileout 8; + filein = fopen("_in64",'I',4,'B'); + fileout = fopen("_out64",'O',3,'B'); + char= '20'x; + do while(fread(filein)=0); + length raw $4 ; + do i=1 to 4; + rc=fget(filein,char,1); + substr(raw,i,1)=char; + end; + rc = fput(fileout, input(raw,$base64X4.)); + rc =fwrite(fileout); + end; + rc = fclose(filein); + rc = fclose(fileout); +run; + +filename _in64 clear; +filename _out64 clear; +