diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 6407f67b..88fcbca7 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -25,7 +25,13 @@ component { */ "moduleRootPath": getCanonicalPath( getCurrentTemplatePath().replaceNoCase( "/ModuleConfig.cfc", "", "one" ) ), /** - * The default storage path for all cbwire components. + * The default storage path for file uploads. + * Uses the system temporary directory for security. + */ + "uploadsStoragePath": getCanonicalPath( getTempDirectory() & "/cbwire" ), + /** + * The default storage path for single-file component compilation. + * This must be in the module directory for WireBox to instantiate components. */ "storagePath": getCanonicalPath( getCurrentTemplatePath().replaceNoCase( "/ModuleConfig.cfc", "", "one" ) & "/models/tmp" ), /** diff --git a/interceptors/CBWIRE.cfc b/interceptors/CBWIRE.cfc index f3448341..6463489e 100644 --- a/interceptors/CBWIRE.cfc +++ b/interceptors/CBWIRE.cfc @@ -25,12 +25,20 @@ component { */ function preReinit() { local.settings = getSettings(); - local.tmpDirectory = local.settings.moduleRootPath & "/models/tmp"; - + + // Clean up single-file component temp directory + local.tmpDirectory = local.settings.storagePath; if ( directoryExists( local.tmpDirectory ) ) { directoryDelete( local.tmpDirectory, true ); directoryCreate( local.tmpDirectory ); } + + // Clean up file uploads temp directory + local.uploadsTmpDirectory = local.settings.uploadsStoragePath; + if ( directoryExists( local.uploadsTmpDirectory ) ) { + directoryDelete( local.uploadsTmpDirectory, true ); + directoryCreate( local.uploadsTmpDirectory ); + } } /** diff --git a/models/CBWIREController.cfc b/models/CBWIREController.cfc index 28e792c8..34c947e9 100644 --- a/models/CBWIREController.cfc +++ b/models/CBWIREController.cfc @@ -133,7 +133,7 @@ component accessors="true" singleton { */ function handleFileUpload( incomingRequest, event ) { // Determine our storage path for temporary files - local.storagePath = getCanonicalPath( variables.moduleSettings.storagePath ); + local.storagePath = getCanonicalPath( variables.moduleSettings.uploadsStoragePath ); // Ensure the storage path exists if( !directoryExists( local.storagePath ) ){ @@ -174,10 +174,10 @@ component accessors="true" singleton { return event.noRender(); } - local.metaPath = getCanonicalPath( variables.moduleSettings.storagePath & "/#local.uuid#.json" ); + local.metaPath = getCanonicalPath( variables.moduleSettings.uploadsStoragePath & "/#local.uuid#.json" ); local.metaJSON = deserializeJSON( fileRead( local.metaPath ) ); - local.contents = fileReadBinary( getCanonicalPath( variables.moduleSettings.storagePath & "/#local.metaJSON.serverFile#" ) ); + local.contents = fileReadBinary( getCanonicalPath( variables.moduleSettings.uploadsStoragePath & "/#local.metaJSON.serverFile#" ) ); event .sendFile( file = local.contents, diff --git a/models/Component.cfc b/models/Component.cfc index 84e02673..0c5132f1 100644 --- a/models/Component.cfc +++ b/models/Component.cfc @@ -466,8 +466,13 @@ component output="true" accessors="true" { } ); } else { var initialState = variables._initialDataProperties; - // Reset individual property - variables.data[ arguments.property ] = initialState[ arguments.property ]; + // Reset individual property (only if it exists in initial state) + if ( initialState.keyExists( arguments.property ) ) { + variables.data[ arguments.property ] = initialState[ arguments.property ]; + } else { + // Property doesn't exist in initial state, set to empty string + variables.data[ arguments.property ] = ""; + } } } diff --git a/models/FileUpload.cfc b/models/FileUpload.cfc index 822590a8..f7298902 100644 --- a/models/FileUpload.cfc +++ b/models/FileUpload.cfc @@ -122,11 +122,51 @@ component { return variables.meta; } + /** + * Moves the file from temporary storage to a permanent location. + * + * The metadata file remains in the uploads temp directory to track the upload state, + * while the actual file is moved to the specified permanent location. This allows + * the FileUpload object to continue tracking the file even after it's been stored. + * + * @path The destination path where the file should be stored (can be a directory or full file path) + * @return The absolute path to the stored file + */ + function store( required string path ){ + var destinationPath = getCanonicalPath( arguments.path ); + + // Check if destination is a directory + if ( directoryExists( destinationPath ) ) { + destinationPath = getCanonicalPath( destinationPath & "/" & variables.meta.serverFile ); + } + + // Ensure the destination directory exists + var destinationDir = getDirectoryFromPath( destinationPath ); + if ( !directoryExists( destinationDir ) ) { + directoryCreate( destinationDir, true ); + } + + // Move the file from temporary to permanent location + fileMove( variables.temporaryStoragePath, destinationPath ); + + // Update the temporary storage path to the new location + variables.temporaryStoragePath = destinationPath; + + // Update metadata to reflect new file location + variables.meta.serverDirectory = destinationDir; + variables.meta.serverFile = getFileFromPath( destinationPath ); + + // Update the metadata file (stays in temp directory for upload tracking) + fileWrite( getMetaPath(), serializeJSON( variables.meta ) ); + + return destinationPath; + } + /** * Returns the path to the temp directory (mockable in tests) */ function getUploadTempDirectory(){ - return getCanonicalPath( variables.moduleSettings.moduleRootPath & "/models/tmp" ); + return getCanonicalPath( variables.moduleSettings.uploadsStoragePath ); } /** diff --git a/test-harness/resources/fileupload_metadata.json b/test-harness/resources/fileupload_metadata.json deleted file mode 100644 index 8a712e02..00000000 --- a/test-harness/resources/fileupload_metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"uuid":"test","fileSize":1234,"contentType":"text","contentSubType":"plain","serverDirectory":"/Users/grantcopley/Code/ortus/cbwire/cbwire/test-harness/tests/resources","serverFile":"logo.png"} \ No newline at end of file diff --git a/test-harness/tests/resources/logo_test.png b/test-harness/tests/resources/logo_test.png new file mode 100644 index 00000000..1febd07e Binary files /dev/null and b/test-harness/tests/resources/logo_test.png differ diff --git a/test-harness/tests/specs/CBWIRESpec.cfc b/test-harness/tests/specs/CBWIRESpec.cfc index 158e5fb2..d3946cca 100644 --- a/test-harness/tests/specs/CBWIRESpec.cfc +++ b/test-harness/tests/specs/CBWIRESpec.cfc @@ -2,12 +2,19 @@ component extends="coldbox.system.testing.BaseTestCase" { function beforeAll() { super.beforeAll(); - // delete any files in models/tmp folder + // delete any files in models/tmp folder (for single-file components) local.tempFolder = expandPath( "../../../models/tmp" ); if ( directoryExists( local.tempFolder ) ) { directoryDelete( local.tempFolder, true ); } directoryCreate( local.tempFolder ); + + // Clean out uploads temp directory + local.uploadsTempFolder = getTempDirectory() & "/cbwire"; + if ( directoryExists( local.uploadsTempFolder ) ) { + directoryDelete( local.uploadsTempFolder, true ); + } + directoryCreate( local.uploadsTempFolder ); } // Lifecycle methods and BDD suites as before... @@ -1077,7 +1084,7 @@ component extends="coldbox.system.testing.BaseTestCase" { { "path": "", "method": "_uploadErrored", - "params": [ "photo", null, false ] + "params": [ "photo", nullValue(), false ] } ], updates = {} diff --git a/test-harness/tests/specs/FileUploadSpec.cfc b/test-harness/tests/specs/FileUploadSpec.cfc index 3c571244..f1cbb436 100644 --- a/test-harness/tests/specs/FileUploadSpec.cfc +++ b/test-harness/tests/specs/FileUploadSpec.cfc @@ -4,7 +4,8 @@ component extends="coldbox.system.testing.BaseTestCase" { super.beforeAll(); // Clean out tmp directory before all tests - local.tempFolder = expandPath( "../../../models/tmp" ); + // Use system temp directory + cbwire subdirectory + local.tempFolder = getTempDirectory() & "/cbwire"; if ( directoryExists( local.tempFolder ) ) { directoryDelete( local.tempFolder, true ); } @@ -81,7 +82,7 @@ component extends="coldbox.system.testing.BaseTestCase" { it( "should return the temporary storage path", function() { var result = loadMockedFileUpload( "test", "text", "plain" ); - expect( result.getTemporaryStoragePath() ).toBe( expandPath( "./resources/logo.png" ) ); + expect( result.getTemporaryStoragePath() ).toBe( expandPath( "./resources/logo_test.png" ) ); }); it( "should return binary file contents when calling get", function() { @@ -99,17 +100,95 @@ component extends="coldbox.system.testing.BaseTestCase" { expect( result.getBase64Src() ).toInclude( "data:" ); }); - it( "should return correct upload temp directory path with proper slash separation", function() { + it( "should return correct upload temp directory path using system temp directory", function() { // Create a fresh FileUpload instance (not mocked) to test getUploadTempDirectory var fileUploadInstance = getInstance( "FileUpload@cbwire" ); var tempDir = fileUploadInstance.getUploadTempDirectory(); - // The path should contain "/models/tmp" with proper slash separation - expect( tempDir ).toInclude( "/models/tmp" ); - // Should not have malformed concatenation like "wiremodels" - expect( tempDir ).notToInclude( "wiremodels" ); - // Should end with models/tmp - expect( right( tempDir, 10 ) ).toBe( "models/tmp" ); + // The path should end with /cbwire + expect( tempDir ).toInclude( "/cbwire" ); + // Should use the system temp directory + expect( tempDir ).toInclude( getTempDirectory() ); + }); + + it( "should store file to a specified directory path", function() { + var result = loadMockedFileUpload( "test-store", "text", "plain" ); + var destinationDir = getTempDirectory() & "/cbwire-test-store"; + + // Ensure the test directory exists + if ( !directoryExists( destinationDir ) ) { + directoryCreate( destinationDir ); + } + + // Store the file + var storedPath = result.store( destinationDir ); + + // Verify the file was moved to the destination + expect( fileExists( storedPath ) ).toBeTrue(); + // Use getCanonicalPath to normalize the path for comparison + expect( storedPath ).toInclude( getCanonicalPath( destinationDir ) ); + expect( storedPath ).toInclude( "logo_test-store.png" ); + + // Clean up + if ( directoryExists( destinationDir ) ) { + directoryDelete( destinationDir, true ); + } + }); + + it( "should store file to a specified file path", function() { + var result = loadMockedFileUpload( "test-store-file", "text", "plain" ); + var destinationDir = getTempDirectory() & "/cbwire-test-store-file"; + var destinationPath = destinationDir & "/myfile.png"; + + // Store the file with specific filename + var storedPath = result.store( destinationPath ); + + // Verify the file was moved to the destination with the new name + expect( fileExists( storedPath ) ).toBeTrue(); + expect( storedPath ).toBe( getCanonicalPath( destinationPath ) ); + + // Clean up + if ( directoryExists( destinationDir ) ) { + directoryDelete( destinationDir, true ); + } + }); + + it( "should update temporary storage path after store", function() { + var result = loadMockedFileUpload( "test-store-update", "text", "plain" ); + var destinationDir = getTempDirectory() & "/cbwire-test-store-update"; + + // Store the file + var storedPath = result.store( destinationDir ); + + // Verify the temporary storage path was updated + expect( result.getTemporaryStoragePath() ).toBe( storedPath ); + + // Clean up + if ( directoryExists( destinationDir ) ) { + directoryDelete( destinationDir, true ); + } + }); + + it( "should be able to destroy file after store", function() { + var result = loadMockedFileUpload( "test-store-destroy", "text", "plain" ); + var destinationDir = getTempDirectory() & "/cbwire-test-store-destroy"; + + // Store the file + var storedPath = result.store( destinationDir ); + + // Verify file exists at new location + expect( fileExists( storedPath ) ).toBeTrue(); + + // Now destroy should delete from the new location + result.destroy(); + + // Verify file was deleted + expect( fileExists( storedPath ) ).toBeFalse(); + + // Clean up directory + if ( directoryExists( destinationDir ) ) { + directoryDelete( destinationDir, true ); + } }); }); @@ -129,12 +208,27 @@ component extends="coldbox.system.testing.BaseTestCase" { ) { var metaPath = expandPath( "../resources/fileupload_metadata.json" ); + // Create a unique copy of logo.png for this test instance + // This ensures each test that calls store() has its own file to move + var uniqueFileName = "logo_" & arguments.uuid & ".png"; + var sourcePath = expandPath( "./resources/logo.png" ); + var destinationPath = expandPath( "./resources/" & uniqueFileName ); + + // Always create a fresh copy for each test + if ( fileExists( sourcePath ) ) { + // Delete destination if it exists (from previous test run) + if ( fileExists( destinationPath ) ) { + fileDelete( destinationPath ); + } + fileCopy( sourcePath, destinationPath ); + } + writeTestMetaFile( path = metaPath, data = { "uuid" : arguments.uuid, "serverDirectory" : expandPath( "./resources" ), - "serverFile" : "logo.png", + "serverFile" : uniqueFileName, "contentType" : arguments.contentType, "contentSubType" : arguments.contentSubType, "fileSize" : 1234