maya get all assets (deadline's script)

```mel
/*
Copyright 2017 Thinkbox Software
All Rights Reserved

This file has the following public functions. Examples of how to use them are below:
get_network_assets
get_filename_attributes_from_scene
get_source_file_paths_from_attr
get_files_to_copy
write_asset_introspection_file_transfer_pairs
gather_source_file_copies
change_to_relative_directory
write_repathing_mel_script_file
repath_assets
find_asset_paths
do_local_asset_caching

Use #1 - Local Asset Caching:
This code is used in production for our "Local Asset Caching". In this case, everything is run on the slave-side, it copies the files, then repaths them.
You can use it directly on the slave-side by calling do_local_asset_caching.
In this case, this file must be executed on the slave-side.

User #2 - Local hot folder synching and remote asset repathing:
This file also contains all the functions needed to do scene introspection and hot folder copying.
This is an example of what we'd do on the submission-side if you wanted to copy all the files into a hot-folder, followed
by creating a script that should run on the slave-side-end which will repath them to the correct place.
It was written for Rodeo FX, but was never put into production.
NOTE: In this case, this MEL script must execute on the SUBMITTER side.
So, if we want to use this code for both 'local asset caching' and this hot-folder thing, we have to have this AssetTools.mel file both on the slave and the submitter.

//Example submitter function:
proc function_to_run_on_submission_for_scene_introspection_and_hot_folder_syncing() {
	
	//This function creates a file called "asset_repathing_script.mel" which must be run on the slave-side to do the repathing.
	
	string $attrNames[0];
	string $cacheFileObjectNames[0];
	string $yetiAttrNames[0];
	string $sourceFiles[0];
	string $destFiles[0];
	string $gatheredSourceFiles[0];

	get_filename_attributes_from_scene( $attrNames, $cacheFileObjectNames, $yetiAttrNames );

	// Only use if you want to filter out files that are already local
	string $networkPrefixes[] = { "//", "/", "X:", "x:", "Y:", "y:", "Z:", "z:" };
	get_network_assets( $attrNames, "basic", $networkPrefixes );
	get_network_assets( $cacheFileObjectNames, "cacheFile", $networkPrefixes );
	get_network_assets( $yetiAttrNames, "yeti", $networkPrefixes );

	get_files_to_copy( "C:/temp", $attrNames, $cacheFileObjectNames, $yetiAttrNames, $sourceFiles, $destFiles );
	gather_source_file_copies("C:/temp", $sourceFiles, $gatheredSourceFiles );
	change_to_relative_directory( $gatheredSourceFiles, "C:/temp");

	write_asset_introspection_file_transfer_pairs( "C:/temp/maya_asset_pairs.txt", $gatheredSourceFiles, $destFiles );
	repath_assets( "C:/temp", $attrNames, $cacheFileObjectNames, $yetiAttrNames  );
	write_repathing_mel_script_file( "C:/temp/asset_repathing_script.mel", "C:/temp", $attrNames, $cacheFileObjectNames, $yetiAttrNames );
}

*/

///////////////////////////////INTERNAL FUNCTIONS////////////////////////////////////

proc int is_windows_path( string $v ) {
	//Starts with "//" (windows network)
	//Starts with "[driveletter]:/" (windows filesystem)
	//Add to this as needed.
	$v = fromNativePath( $v );
	if( size($v) < 4 ) //min size of four
		return false;
	$matchVal = `match ".*\n.*" $v`; //no newlines. important to add because of embedded mel in string attributes.
	if( $matchVal != "" )
		return false;
	if( startsWith( $v, "//" ) && !startsWith( $v, "///" ) ) //double slash starting. good.
		return true;
	$matchVal = `match "^[a-zA-Z]:/.*" $v`; //windows drive letter style. good. absolute paths only.
	if( $matchVal != "" )
		return true;
	return false;
}

proc int is_posix_path( string $v ) {
	//Starts with "/" (linux filesystem)
	//Add to this as needed.
	$v = fromNativePath( $v );
	if( size($v) < 2 ) //min size of two
		return false;
	$matchVal = `match ".*\n.*" $v`; //no newlines. important to add because of embedded mel in string attributes.
	if( $matchVal != "" )
		return false;
	if( startsWith( $v, "/" ) && !startsWith( $v, "//" ) ) //must start with single slash. only absolute paths supported (can't have a double slash. that's windows)
		return true;
	return false;
}

proc string to_native_path( string $v ) {
	//This is used to swap slashes on windows.
	//There is already a MEL script function called toNativePath, but swaps to the filename types of the current OS. We want to do it based on the actual style of path instead.
	//NOTE: MEL's "fromNativePath" ALWAYS converts everything to forward slashes, but "toNativePath" does it based on the current OS.
	if( is_windows_path( $v ) )
		while( $v != ( $v = `substitute "/" $v "\\"` ) ); //swap to backslashes
	return $v;
}

proc int looks_like_filepath( string $attrVal ) {
	//check all the cases for absolute filenames:
	if( is_windows_path( $attrVal ) )
		return true;
	if( is_posix_path( $attrVal ) )
		return true;
	return false;
}

proc string get_asset_path( string $attrName ) {
	//Get a filename out of a string attribute
	//Normally this is just the value of the attribute.
	//HOWEVER there are special exceptions that we may need to handle here.
	//An example might be filenames that use "####" in place of the current frame number or something. An example is Krakatoa objects. However, this isn't handled yet.

	//Normal case, just use the string attribute itself. Most things will end up here.
	$attrVal = `getAttr $attrName`; //should return string.
	
	//Just exclude the empty string case right away
	if( $attrVal == "" )
		return "";

	//This swaps backslashes to forward slashes
	$attrVal = fromNativePath( $attrVal );

	if( looks_like_filepath( $attrVal ) )
		return $attrVal;

	return "";
}

proc int is_attribute_a_string( string $nodeAndAttrName ) {
	//for some reason, some attributes produce a "The value for the attribute could not be retrieved" when trying to query them. If that's the case, then just skip it. I don't think this'll be an issue.
	if( catchQuiet( `getAttr -type $nodeAndAttrName` ) ) {
		return false;
	}

	$attrType = `getAttr -type $nodeAndAttrName`;
	if( $attrType == "string" ) {
		return true;
	}
	return false;
}

proc int is_multi_attribute_a_string( string $nodeName, string $attrNameComponents[] ) {
	//Test whether this multi-attribute holds strings
	//Without this test prior to recursion, we'll be recursively iterating through a lot of useless non-string attributes. For example, mesh vertices.
	//For example, recurse to the end of "tweak1.plist[0].controlPoints[0].xValue" and determine if "xValue" is a string attribute.
	
	$multiAttrName = $nodeName;
	for( $i=0; $i<size($attrNameComponents)-1; $i++ ) {
		$multiAttrName += "." + $attrNameComponents[$i];
		int $indexArray[] = `getAttr -multiIndices $multiAttrName`;
		if( size($indexArray) == 0 )
			return false; //this array of multi-attributes is empty. we don't need to continue.
		$multiAttrName += "[" + $indexArray[0] + "]"; //add the first index in this multi-attribute
	}
	$multiAttrName += "." + $attrNameComponents[ size($attrNameComponents)-1 ]; //add the actual non-multi-portion of the attribute to the end

	return is_attribute_a_string( $multiAttrName );
}

proc int is_attribute_storable( string $attrName )
{
	string $attrComponents[] = stringToStringArray( $attrName, "." );
	int $attrArraySize = `size($attrComponents)`;
	string $attrComponentAttr = $attrComponents[ $attrArraySize - 1 ];
	stringArrayRemoveAtIndex( $attrArraySize - 1, $attrComponents );
	string $attrComponentBase = stringArrayToString( $attrComponents, "." );
	
	return `attributeQuery -st -node $attrComponentBase $attrComponentAttr`;
}

proc get_multi_attrs_recursive( string $outAttrs[], string $attrNameBase, string $attrNameComponents[], int $level ) {
	//Examples:
	//file1.explicitUvTiles[0].explicitUvTileName
	//file1.explicitUvTiles[1].explicitUvTileName
	//tweak1.plist[0].controlPoints[0].xValue

	$attrName = $attrNameBase + "." + $attrNameComponents[$level];

	if( $level == size($attrNameComponents) - 1 ) {
		$outAttrs[ size($outAttrs) ] = $attrName;
		return;
	}
	
	int $indexArray[] = `getAttr -multiIndices $attrName`;
	for( $i=0; $i<size($indexArray); $i++ ) {
		$attrNameWithIndex = $attrName + "[" + $indexArray[$i] + "]";
		get_multi_attrs_recursive( $outAttrs, $attrNameWithIndex, $attrNameComponents, ($level+1) );
	}
}

proc string[] get_all_string_attributes_from_node( string $nodeName ) {

	string $outAttrs[];

	$attrNameList = `listAttr -usedAsFilename $nodeName`;

	for( $i=0; $i<size($attrNameList); $i++ ) {

		$attrName = $attrNameList[$i];
		
		//See if this is a "multi-attribute" by splitting on "."
		//Example: $attrName = "explicitUvTiles.explicitUvTileName"
		//Example: $attrName = plist.controlPoints.xValue... This needs to be accessed by tweak1.plist[0].controlPoints[0].xValue
		$attrNameComponents = stringToStringArray( $attrName, "." );

		if( size($attrNameComponents) >= 2 ) {

			//multi-attribute case. must use recursion to get the attributes.
			if( is_multi_attribute_a_string( $nodeName, $attrNameComponents ) ) {
				get_multi_attrs_recursive( $outAttrs, $nodeName, $attrNameComponents, 0 ); //will append to $outAttrs
			}

		} else {

			//Normal (non-multi) attribute case
			//Example: "file1.fileTextureName"
			string $fullNodeAndAttrName = $nodeName + "." + $attrName;

			if( is_attribute_a_string( $fullNodeAndAttrName ) ) {
				$outAttrs[ size($outAttrs) ] = $fullNodeAndAttrName; //append to $outAttrs
			}

		}
	}

	return $outAttrs;
}

proc string[] get_all_string_attributes_from_scene() {

	//The values in the outgoing array look like "nodeName.attrName" or "nodeName.multiAttrName[index].attrName", etc.

	string $allStringAttrs[0];

	$allSceneNodes = `ls`;
	for( $i=0; $i<size($allSceneNodes); $i++ ) {
		$nodeName = $allSceneNodes[$i];

		//Certain nodes and handled with special cases, and should be ignored here.
		//For example, "cacheFile" type objects use both a "Base Directory" and a "Base Name" attributes to construct a full filename.
		//So we handle those nodes separately later on.
		$nodeType = `objectType $nodeName`;
		if( $nodeType == "cacheFile" )
			continue;

		string $stringAttrs[] = `get_all_string_attributes_from_node $nodeName`;

		//append these to the outgoing array
		appendStringArray( $allStringAttrs, $stringAttrs, size($stringAttrs) );
	}

	return $allStringAttrs;
}

proc int check_if_file_exists( string $filename, string $nameOfSourceAttr ) {
	if( $filename == "" )
		return false;
	if( !`filetest -f $filename` ) {
		if( `filetest -d $filename` )
			print( "WARNING: Skipping potential introspection file. The following attribute appears to be a directory, and not a filename: " + $nameOfSourceAttr + " = '" + $filename + "'\n" );
		else
			print( "WARNING: Skipping potential introspection file. The following attribute looks like a filename, however the file does not exist: " + $nameOfSourceAttr + " = '" + $filename + "'\n" );
		return false;
	}
	return true;
}

proc get_all_filename_attributes_from_scene( string $outAttrNames[] ) {

	//This gives us all string attributes in the scene.
	//Both returned arrays are the same length. Together they make pairs.
	//The values in here look like "nodeName.attrName" or "nodeName.multiAttrName[index].attrName", etc.
	//Each one is queryable as-is and will return a string.
	string $stringAttrArray[] = `get_all_string_attributes_from_scene`;

	for( $i=0; $i<size($stringAttrArray); $i++ ) {
		$stringAttr = $stringAttrArray[$i];

		//Get the value out of this attribute.
		$filenameValue = get_asset_path( $stringAttr );

		//Test to see if this file actually exists.
		//The above code could have caught directories, etc. or just strings that look like filenames.
		// udim files won't be considered files since the token isn't evaluated
		if( `gmatch $filenameValue "*<udim>*"` || `gmatch $filenameValue "*<UDIM>*"` || check_if_file_exists( $filenameValue, $stringAttr ) ) {
			$outAttrNames[ size($outAttrNames) ] = $stringAttr; //append
		}
	}
}

proc get_all_filename_attributes_from_cacheFile_objects( string $outAttrNames[] ) {
	//Nodes of type cacheFiles are intentionally skipped over in the attribute introspection code.
	//That is because they present a special case where there are two files (.mcx, .xml) that are derived from a base directory and base name.
	//I'm not 100% sure if it's always a .mcx file that it needs, but in our current case, it is. There always appears to be a .xml file too.

	string $allCacheNodeNames[] = `ls -type "cacheFile"`;
	for( $i=0; $i<size($allCacheNodeNames); $i++ ) {
		$outAttrNames[ size( $outAttrNames ) ] = $allCacheNodeNames[$i];
	}
}

proc get_all_filename_attributes_from_yeti_objects( string $outYetiAttrNames[] ) {
	//http://peregrinelabs-deploy.s3.amazonaws.com/Documentation/Yeti/1.3.16/scriptingref.html

	//Currently we're supporting Yeti's texture node's "file_name" attribute, and Yeti's reference node's "reference_file" attribute.
	//Maybe in the future this could be expanded to be more general if needed.

	string $yetiShapesArray[] = `ls -type "pgYetiMaya"`;
	string $yetiShapesArrayA[] = `ls -type "pgYetiMayaFeather"`; //I really think only pgYetiMaya nodes can have a Yeti graph. But I don't know. Maybe these other two types can too. I have them in here just for safe measure.
	string $yetiShapesArrayB[] = `ls -type "pgYetiGroom"`;
	appendStringArray( $yetiShapesArray, $yetiShapesArrayA, size($yetiShapesArrayA) );
	appendStringArray( $yetiShapesArray, $yetiShapesArrayB, size($yetiShapesArrayB) );

	for( $i=0; $i<size($yetiShapesArray); $i++ ) {
		$yetiShape = $yetiShapesArray[$i];

		string $yetiTextureNodeList[0];
		if( catch( $yetiTextureNodeList = `pgYetiGraph -listNodes -type "texture" $yetiShape` ) ) {
			print( "WARNING: There are Yeti nodes in the scene, but the Yeti plugin does not appear to be loaded. Texture files within the Yeti nodes will not be repathed.\n" );
			return;
		}
		for( $j=0; $j<size($yetiTextureNodeList); $j++ ) {
			$yetiTextureNode = $yetiTextureNodeList[$j];
			$attrVal = `pgYetiGraph -node $yetiTextureNode -param "file_name" -getParamValue $yetiShape`;
			$attrName = $yetiShape + "." + $yetiTextureNode + ".file_name"; //Make it a period separted attribute name. This is just for convenience. Later on in the code we will split this out into 3 parts again.
			if( check_if_file_exists( $attrVal, $attrName ) ) {
				$outYetiAttrNames[ size($outYetiAttrNames) ] = $attrName;
			}
		}

		$yetiReferenceNodeList = `pgYetiGraph -listNodes -type "reference" $yetiShape`;
		for( $j=0; $j<size($yetiReferenceNodeList); $j++ ) {
			$yetiReferenceNode = $yetiReferenceNodeList[$j];
			$attrVal = `pgYetiGraph -node $yetiReferenceNode -param "reference_file" -getParamValue $yetiShape`;
			$attrName = $yetiShape + "." + $yetiReferenceNode + ".reference_file"; //Make it a period separted attribute name. This is just for convenience. Later on in the code we will split this out into 3 parts again.
			if( check_if_file_exists( $attrVal, $attrName ) ) {
				$outYetiAttrNames[ size($outYetiAttrNames) ] = $attrName;
			}
		}
	}
}

proc string asset_repathing_translation( string $sourcePath, string $repathRoot ) {
	//Here is the rules this function uses. Dest paths can be Windows or Posix. The input path always satisfies one of these three:
	// 1) C:/win/path/myfile.ext > /repath_root/C/win/path/myfile.ext
	// 2) //netdrive/win/path/myfile.ext > /repath_root/netdrive/win/path/myfile.ext
	// 3) /posix/path/myfile.ext > /repath_root/posix/path/myfile.ext

	//Potential trouble:
	//Some characters that are valid in Windows may not be valid in Posix paths. I'm just assuming here that all the characters are allowable in both systems.
	//Maybe need to do some additional mapping here to make it work.
	
	if( !looks_like_filepath( $repathRoot ) ) {
		print( "ERROR: Could not repath to here because it is not a valid absolute path: '" + $repathRoot + "'\n" );
		return "";
	}

	$repathRoot = fromNativePath( $repathRoot );
	if( substring( $repathRoot, size($repathRoot), size($repathRoot) ) == "/" ) //strip trailing slash
		$repathRoot = substring( $repathRoot, 1, size($repathRoot)-1 );

	$outPath = "";
	if( is_windows_path( $sourcePath ) ) {

		$winPath = "";
		if( startsWith( $sourcePath, "//" ) ) {
			$winPath = substring( $sourcePath, 3, size($sourcePath) ); //strip double-slash
		} else {
			//remove the ":", so it looks like "/repath_root/C/previous_path/filename.ext"
			$winPath = toupper( substring( $sourcePath, 1, 1 ) ) + substring( $sourcePath, 3, size($sourcePath) );
		}
		$outPath = $repathRoot + "/" + $winPath;

	} else { //is_posix_path() will return true here, because was checked earlier to be one or the other.
		$outPath = $repathRoot + $sourcePath;
	}
	return $outPath;
}

proc int is_local_file( string $path, string $networkPrefixes[] ) {
	int $isLocal = true;

	for( $i=0; $i<size($networkPrefixes); $i++ ) {
		if( startsWith( $path, $networkPrefixes[$i] ) ) {
			$isLocal = false;
			break;
		}
	}

	return $isLocal;
}

proc string[] get_cacheFile_paths( string $nodeName ) {
	string $cachePathAttr = $nodeName + ".cachePath";
	string $baseNameAttr = $nodeName + ".cacheName";
	string $baseName = `getAttr $baseNameAttr`;
	string $cachePath = `getAttr $cachePathAttr`;
	$cachePath = fromNativePath( $cachePath );

	//combine path and base name. make sure there's a "/" separator
	string $lastChar = `substring $cachePath (size($cachePath)) (size($cachePath))`;
	if( $lastChar != "/" )
		$cachePath = $cachePath + "/";
	$fullPathWithoutExt = $cachePath + $baseName;

	$mcxFilename = $fullPathWithoutExt + ".mcx";
	$xmlFilename = $fullPathWithoutExt + ".xml";

	return {$mcxFilename, $xmlFilename};
}

proc string get_cacheFile_directory( string $nodeName ) {
	string $cachePathAttr = $nodeName + ".cachePath";
	string $cachePath = `getAttr $cachePathAttr`;
	$cachePath = fromNativePath( $cachePath );

	//combine path and base name. make sure there's a "/" separator
	string $lastChar = `substring $cachePath (size($cachePath)) (size($cachePath))`;
	if( $lastChar != "/" )
		$cachePath = $cachePath + "/";

	return $cachePath;
}

proc string get_yeti_path( string $nodeName ) {
	string $yetiValues[];

	$yetiValues = stringToStringArray( $nodeName, "." ); // 3 period separated values
	return `pgYetiGraph -node $yetiValues[1] -param $yetiValues[2] -getParamValue $yetiValues[0]`;
}

proc translate_all_asset_files( string $repathRoot, string $attrNames[], string $cacheFileObjectNames[], string $yetiAttrNames[], string $outAttrFiles[], string $outCacheFiles[], string $outYetiFiles[] ) {
	clear($outAttrFiles);
	clear($outCacheFiles);
	clear($outYetiFiles);

	// translate attribute paths 
	for( $i=0; $i<size($attrNames); $i++ ) {
		$outAttrFiles[$i] = asset_repathing_translation( get_asset_path( $attrNames[$i] ), $repathRoot );
	}

	// translate cacheFile paths
	for( $i=0; $i<size($cacheFileObjectNames); $i++ ) {
		$outCacheFiles[$i] = asset_repathing_translation( get_cacheFile_directory( $cacheFileObjectNames[$i] ), $repathRoot );
	}

	// translate yeti paths
	for( $i=0; $i<size($yetiAttrNames); $i++ ) {
		$outYetiFiles[$i] = asset_repathing_translation( get_yeti_path( $yetiAttrNames[$i] ), $repathRoot );
	}
}

proc int manifest_file_dirty( string $manifestPath, string $sourceFile, string $destFile )
{
	// given an existing manifest path and two existing asset paths (cache and network), compare the timestamps of the manifest and the files.
	// the first line of the manifest file will be the cacheFile's timestamp, the second is the networkFile's timestamp
	python( "import os" );
	string $sourceModifiedtime = python( "os.path.getmtime('" + $sourceFile + "')" );
	string $destModifiedtime = "";
	if( catch( $destModifiedtime = python( "os.path.getmtime('" + $destFile + "')" ) ) )
	{
		print( "Failed to get modified time from file: " + $destFile );
		return true;
	}

	$fileId = `fopen $manifestPath "r"`;

	$sourceManifestTime = `fgetline $fileId`;
	$destManifestTime = `fgetline $fileId`;
	fclose $fileId;

	$sourceManifestTime = substituteAllString($sourceManifestTime, "\n", "");
	$destManifestTime = substituteAllString($destManifestTime, "\n", "");

	return !( ($sourceModifiedtime == $sourceManifestTime) && ($destModifiedtime == $destManifestTime ) );
}

// Walk over manifest files in a directory and delete the old assets
proc inspect_manifests( string $manifestFiles[], int $deleteTimer )
{
	python( "import time" );
	int $manifestSize_CONST = 9; // ".manifest" has 9 characters
	int $deleteTimerInSec = $deleteTimer * 24 * 60 * 60;
	for( $manifest in $manifestFiles )
	{
		$fileId = `fopen $manifest "r"`;
		string $filedata = `fread $fileId $filedata`;
		string $fileLines[];
		tokenize $filedata "\n" $fileLines;

		int $manifestTime =  int( $fileLines[size($fileLines)-1] );
		fclose $fileId;

		int $currTime = python( "int(time.time())" );
		//print( "elapsed=" + ( $currTime - $manifestTime ) + " required=" + $deleteTimerInSec + "\n" );
		if( $currTime - $manifestTime > $deleteTimerInSec )
		{
			string $assetName = `substring $manifest 1 (size($manifest)-$manifestSize_CONST)`;
			print( "Deleting asset file " + $assetName + " because it is more than " + $deleteTimer + " days old\n" );
			sysFile -delete $assetName;
			sysFile -delete $manifest;
		}
	}
}

proc delete_assets( string $cacheDir, int $deleteTimer ) {

	string $manifests[] = `getFileList -folder $cacheDir -fs "*.manifest"`;
	for( $i = 0; $i < size($manifests); $i++ )
	{
		$manifests[$i] = $cacheDir + "/" + $manifests[$i];
	}
	string $folders[] = `getFileList -folder $cacheDir`;
	for( $folder in $folders )
	{
		string $currFolder = $cacheDir + "/" + $folder;
		delete_assets( $currFolder, $deleteTimer );
	}
	inspect_manifests( $manifests, $deleteTimer );
}

////////////////////// EXTERNAL FUNCTIONS TO BE USED FOR HOT-FOLDER SYNCHING AND REPATH, AND LOCAL ASSET CACHING //////////////////

// Using $networkPrefixes we filter out all assets that don't have a network prefix so that we only mess with network files
global proc get_network_assets( string $outAttrNames[], string $type, string $networkPrefixes[] ) {
	string $filenameValue;

	for( $i=size($outAttrNames)-1; $i>=0; $i-- ) {
		if( $type == "cacheFile" ) {
			string $filenames[] = get_cacheFile_paths( $outAttrNames[$i] );
			// Only need to check 1 path, as they have the same folder
			$filenameValue = $filenames[0];
		} else if( $type == "yeti" ) {
			$filenameValue = get_yeti_path( $outAttrNames[$i] );
		} else { // basic
			$filenameValue = get_asset_path( $outAttrNames[$i] );
		}

		if( is_local_file( $filenameValue, $networkPrefixes ) ) {
			stringArrayRemoveAtIndex( $i, $outAttrNames );
		}
	}
}

global proc get_filename_attributes_from_scene( string $outAttrNames[], string $outCacheFileObjectNames[], string $outYetiAttrNames[] ) {
	clear( $outAttrNames );
	clear( $outCacheFileObjectNames );
	clear( $outYetiAttrNames );

	//First get name/value pairs from string attributes in the scene
	//This will be the majority of all assets. Special case attributes to follow.
	get_all_filename_attributes_from_scene( $outAttrNames );

	//Special case alert: Nodes of type "cacheFile" need to be special caseed because they combine two attributes to make a path.
	get_all_filename_attributes_from_cacheFile_objects( $outCacheFileObjectNames );

	//Special case the Yeti nodes because you have to get attributes differently.
	if( `pluginInfo -query -loaded "pgYetiMaya"` ){
		get_all_filename_attributes_from_yeti_objects( $outYetiAttrNames );
	}
}

global proc get_source_file_paths_from_attr( string $outAttrNames[], string $outCacheFileObjectNames[], string $outYetiAttrNames[], string $outSourceFiles[] ){
	python( "import maya.app.general.fileTexturePathResolver" );
	
	// Build list of files
	for( $i=0; $i<size($outAttrNames); $i++ ) {
		$sourceFile = get_asset_path( $outAttrNames[$i] );
		
		// check for tokens in file pattern
		string $computedFileAttr = `substitute "fileTextureName" $outAttrNames[$i] "computedFileTextureNamePattern"`;
		if (`objExists $computedFileAttr`) {
			string $seq_pattern = `getAttr($computedFileAttr)`;
			string $filePaths[] = python( "maya.app.general.fileTexturePathResolver.findAllFilesForPattern( '" + $seq_pattern + "', None )" );
			for( $j=0; $j<size($filePaths); $j++) {
				$outSourceFiles[ size( $outSourceFiles ) ] = $filePaths[$j];
			}
		} else {
			$outSourceFiles[ size( $outSourceFiles ) ] = $sourceFile;
		}
	}

	for( $i=0; $i<size($outCacheFileObjectNames); $i++ ) {
		appendStringArray( $outSourceFiles, get_cacheFile_paths( $outCacheFileObjectNames[$i] ), 2 );
	}
	for( $i=0; $i<size($outYetiAttrNames); $i++ ) {
		$outSourceFiles[ size( $outSourceFiles ) ] = get_yeti_path( $outYetiAttrNames[$i] );
	}
	$outSourceFiles = stringArrayRemoveDuplicates( $outSourceFiles );
}

global proc get_files_to_copy( string $repathRoot, string $outAttrNames[], string $outCacheFileObjectNames[], string $outYetiAttrNames[], string $outSourceFiles[], string $outDestFiles[] ){
	get_source_file_paths_from_attr( $outAttrNames, $outCacheFileObjectNames, $outYetiAttrNames, $outSourceFiles );

	//Translate the paths, and put into our outgoing-variable $outDestFiles
	for( $i=0; $i<size($outSourceFiles); $i++ ) {
		$outDestFiles[$i] = asset_repathing_translation( $outSourceFiles[$i], $repathRoot );
	}
}

global proc get_redshift_cache_files( string $sourceFiles[], string $outRedshiftCacheFiles[] ) {
	
	for( $sourceFile in $sourceFiles ){
		string $sourceDirectory = dirname( $sourceFile );
		string $oldExtension = fileExtension( $sourceFile );
		string $filename = basename( $sourceFile, $oldExtension );
		string $cacheFiles[] = `getFileList -folder $sourceDirectory -filespec ($filename+"rs*bin")`;
		for( $cacheFile in $cacheFiles )
		{
			$outRedshiftCacheFiles[ size( $outRedshiftCacheFiles ) ] = ( $sourceDirectory +"/"+ $cacheFile );
		}
	}
}

global proc write_asset_introspection_file_transfer_pairs( string $outputSourceDestPairsFilename, string $sourceFiles[], string $destFiles[] ) {

	print( "ASSETS TRANSFERED: " + size( $sourceFiles ) + "\n" );
	if( size( $sourceFiles ) > 0 ) {
		$outputSourceDestPairsFilename = fromNativePath( $outputSourceDestPairsFilename ); //Sanitize
		$fileId = `fopen $outputSourceDestPairsFilename "w"`;

		for( $i=0; $i<size($sourceFiles); $i++ ) {
			//swap to windows-style backslashes for the output of all this. currently they're all forward slashes.
			$source = $sourceFiles[$i];
			$dest = $destFiles[$i];
			$source = to_native_path( $source ); //swap to backslashes if necessary
			$dest = to_native_path( $dest );

			$fileLineStr = ( $source + ">" + $dest + "\n" );
			fprint $fileId $fileLineStr;
			print( $fileLineStr ); //For debug
		}

		fclose $fileId;
	}
}

global proc gather_source_file_copies( string $gatherDirectoryRoot, string $inOrigSourceFiles[], string $outNewSourceFiles[] ) {

	//NOTE: On error (couldn't copy for some reason), an error message is printed, and the original path to the file is returned.
	//We might need to change this behaviour to handle errors better/differently.

	//Sanitize inputs
	clear( $outNewSourceFiles );
	$gatherDirectoryRoot = fromNativePath( $gatherDirectoryRoot );
	if( substring( $gatherDirectoryRoot, size($gatherDirectoryRoot), size($gatherDirectoryRoot) ) != "/" ) //add trailing slash
		$gatherDirectoryRoot += "/";

	for( $i=0; $i<size($inOrigSourceFiles); $i++ ) {

		$sourceFile = fromNativePath( $inOrigSourceFiles[$i] );
		$destFile = "";

		$destFile = asset_repathing_translation( $sourceFile, $gatherDirectoryRoot );
		string $destDirectory = python( "import os; os.path.dirname('" + $destFile + "')" ); //get directory part
		
		$dirSuccess = true;
		if( !`filetest -d $destDirectory` ) {
			$pythonScriptMakeDir = "import os; os.makedirs('" + $destDirectory + "')"; //recursive dir creation
			if( catch( python( $pythonScriptMakeDir ) ) ) {
				print( "ERROR: Could not create the gathering destination directory: '" + $destFile + "'\n" );
				$destFile = $sourceFile;
				$dirSuccess = false;
			}
		}
		
		if( $dirSuccess ) { // manifest is of the form src \n dest ( network \n cache )
			$doCopy = false;
			$manifestPath = $destFile + ".manifest";
			// if manifest non existant or manifest dirty
			// something is wrong with checking the status of the file
			if( !(`file -q -exists $manifestPath`) || manifest_file_dirty( $manifestPath, $sourceFile, $destFile) ) {
				print("Manifest file outdated... Updating asset cache.\n");
				$doCopy = true;
			}
			else { // manifest file exists and is clean
				print("Manifest file is up-to-date... Rewriting last accessed time.\n");
				// write back manifest data with updated access time
				python( "import time" );

				// grab the first two lines to write them back to the file
				$fileId = `fopen $manifestPath "r"`;

				$sourceManifestTime = `fgetline $fileId`;
				$destManifestTime = `fgetline $fileId`;
				fclose $fileId;

				$fileId = `fopen $manifestPath "w"`;
				string $currTime = python( "int(time.time())" );
				fprint $fileId $sourceManifestTime;
				fprint $fileId $destManifestTime;
				fprint $fileId $currTime;
				fclose $fileId;
			}
		
			if( $doCopy ) {
				$copySuccess = `sysFile -copy $destFile $sourceFile`;
				if( !$copySuccess ) {
					print( "ERROR: Could not copy file from '" + $sourceFile + "' to '" + $destFile + "'\n" );
					$destFile = $sourceFile;
				} else {
					print( "File copied from '" + $sourceFile + "' to '" + $destFile + "'\n" );
					// (over)write manifest file. sourcetime \n desttime
					$fileId = `fopen $manifestPath "w"`;
					python( "import os, time" );

					string $newSourceManifest = python("os.path.getmtime('" + $sourceFile + "')");
					string $newDestManifest = python("os.path.getmtime('" + $destFile + "')");
					string $currTime = python( "int(time.time())" );

					fprint $fileId ($newSourceManifest+"\n");
					fprint $fileId ($newDestManifest+"\n");
					fprint $fileId $currTime;
					// set line 3 to current time
					fclose $fileId;
				}
			}
		}
		$outNewSourceFiles[$i] = to_native_path( $destFile );
	}
}

global proc change_to_relative_directory( string $fileArray[], string $rootDir ) {
	//Convenience function. Turns absolute paths into relative paths based on the root directory.

	$rootDir = fromNativePath( $rootDir );
	if( substring( $rootDir, size($rootDir), size($rootDir) ) != "/" ) //add trailing slash
		$rootDir += "/";

	for( $i=0; $i<size($fileArray); $i++ ) {
		$file = fromNativePath( $fileArray[$i] );
		if( startsWith( $file, $rootDir ) ) {
			$newFile = substring( $file, size($rootDir)+1, size($file) );
			if( is_windows_path( $file ) ) {
				while( $newFile != ( $newFile = `substitute "/" $newFile "\\"` ) ); //swap to backslashes
			}
			$fileArray[$i] = $newFile;
		}
	}
}

global proc write_repathing_mel_script_file( string $outputMelScriptRepathingFilename, string $repathRoot, string $attrNames[], string $cacheFileObjectNames[], string $yetiAttrNames[] ) {
	string $translatedAttrFiles[];
	string $translatedCacheFiles[];
	string $translatedYetiFiles[];

	translate_all_asset_files( $repathRoot, $attrNames, $cacheFileObjectNames, $yetiAttrNames, $translatedAttrFiles, $translatedCacheFiles, $translatedYetiFiles );

	$fileId = `fopen $outputMelScriptRepathingFilename "w"`;

	// repath attribute paths
	for( $i=0; $i<size($attrNames); $i++ ) {
		if( is_attribute_storable( $attrNames[$i] ) ) {

			if( `gmatch $translatedAttrFiles[$i] "*<udim>*"` || `gmatch $translatedAttrFiles[$i] "*<UDIM>*"`|| `filetest -f $translatedAttrFiles[$i]` ) {
				$fileLineStr = "catch( `setAttr -type \"string\" \"" + $attrNames[$i] + "\" \"" + $translatedAttrFiles[$i] + "\"` );\n";
				fprint $fileId $fileLineStr;
			}
		}
	}

	// repath cacheFile directories
	for( $i=0; $i<size($cacheFileObjectNames); $i++ ) {
		string $cachPathAttr = $cacheFileObjectNames[$i] + ".cachePath";
		if(`attributeQuery -st -node $cacheFileObjectNames[$i] "cachePath"` ) {
			if( `filetest -f $translatedCacheFiles[$i]` ) {
				$fileLineStr = "catch( `setAttr -type \"string\" \"" + $cachPathAttr + "\" \"" + $translatedCacheFiles[$i] + "\"` );\n";
				fprint $fileId $fileLineStr;
			}
		}
	}

	// repath yeti
	for( $i=0; $i<size($yetiAttrNames); $i++ ) {
		string $attrComponents[];
		$attrComponents = stringToStringArray( $yetiAttrNames[$i], "." ); //Always size 3 because we made these in the above code.
		$attrValue = $translatedYetiFiles[$i];
		$shape = $attrComponents[0];
		$node = $attrComponents[1];
		$attr = $attrComponents[2];

		if( `filetest -f $attrValue` ) {
			$fileLineStr = "catch( `pgYetiGraph -node \"" + $node + "\" -param \"" + $attr + "\" -setParamValueString \"" + $attrValue + "\" \"" + $shape + "\"` );\n";
			fprint $fileId $fileLineStr;
		}
	}

	fclose $fileId;
}

global proc repath_assets( string $repathRoot, string $attrNames[], string $cacheFileObjectNames[], string $yetiAttrNames[] ) {
	string $translatedAttrFiles[];
	string $translatedCacheFiles[];
	string $translatedYetiFiles[];

	translate_all_asset_files( $repathRoot, $attrNames, $cacheFileObjectNames, $yetiAttrNames, $translatedAttrFiles, $translatedCacheFiles, $translatedYetiFiles );

	// repath attribute paths
	for( $i=0; $i<size($attrNames); $i++ ) {
		if( is_attribute_storable( $attrNames[$i] ) ){
			if( `gmatch $translatedAttrFiles[$i] "*<udim>*"` || `gmatch $translatedAttrFiles[$i] "*<UDIM>*"`|| `filetest -f $translatedAttrFiles[$i]` ) {
				catch( `setAttr -type "string" $attrNames[$i] $translatedAttrFiles[$i]` );
			}
		}
	}

	// repath cacheFile directories
	for( $i=0; $i<size($cacheFileObjectNames); $i++ ) {
		string $cachPathAttr = $cacheFileObjectNames[$i] + ".cachePath";
		if(`attributeQuery -st -node $cacheFileObjectNames[$i] "cachePath"` ) {
			if( `filetest -f $translatedCacheFiles[$i]` ) {
				catch( `setAttr -type "string" $cachPathAttr $translatedCacheFiles[$i]` );
			}
		}
	}

	// repath yeti
	for( $i=0; $i<size($yetiAttrNames); $i++ ) {
		string $attrComponents[];
		$attrComponents = stringToStringArray( $yetiAttrNames[$i], "." ); //Always size 3 because we made these in the above code.
		$attrValue = $translatedYetiFiles[$i];
		$shape = $attrComponents[0];
		$node = $attrComponents[1];
		$attr = $attrComponents[2];

		if( `filetest -f $attrValue` ) {
			catch( `pgYetiGraph -node $node -param $attr -setParamValueString $attrValue $shape` );
		}
	}
}

global proc delete_old_assets( string $cacheDir, int $deleteTimer )
{
		string $cachePaths[];
		tokenize $cacheDir ">" $cachePaths;
		delete_assets( $cachePaths[0], $deleteTimer );
}

string $dl_ignorable_asset_attributes[] = {"rmanGlobals.imageOutputDir","rmanGlobals.ribOutputDir"};
proc int is_ignorable_attribute( string $attr )
{
	global string $dl_ignorable_asset_attributes[];
	return `stringArrayContains $attr $dl_ignorable_asset_attributes`;
}

global proc string[] find_asset_paths()
{
	filePathEditor -refresh;
	string $file_paths[];
	string $dirs[] = `filePathEditor -q -listDirectories ""`;
	for( $dir in $dirs )
	{
		string $files[] = `filePathEditor -q -withAttribute -listFiles $dir`;
		int $i;
		for( $i = 0; $i<size($files); $i+=2)
		{
			if( !`is_ignorable_attribute $files[$i+1]` )
			{
				string $file_path = $dir + "/" + $files[$i];
				print("Found Asset: " + $files[$i+1] + " - " + $file_path + "\n" );
				$file_paths[ size($file_paths) ] = $dir + "/" + $files[$i]; //append
			}
		}
	}

	return $file_paths;
}

global proc do_local_asset_caching( string $localAssetCacheDirectory, string $semiColonSeparatedNetworkPrefixes, string $slaveLACDaysToDelete )
{
	string $attrNames[0];
	string $cacheFileObjectNames[0];
	string $yetiAttrNames[0];
	string $sourceFiles[0];
	string $redshiftCacheFiles[0];
	string $destFiles[0];
	string $gatheredSourceFiles[0];
	string $gatheredRedshiftSourceFiles[0];

	//identify all attributes that contain filenames
	get_filename_attributes_from_scene( $attrNames, $cacheFileObjectNames, $yetiAttrNames );

	//of the identified files above, filter out the ones that aren't in a "network" location.
	string $networkPrefixes[] = stringToStringArray( $semiColonSeparatedNetworkPrefixes, ";" );
	get_network_assets( $attrNames, "basic", $networkPrefixes );
	get_network_assets( $cacheFileObjectNames, "cacheFile", $networkPrefixes );
	get_network_assets( $yetiAttrNames, "yeti", $networkPrefixes );

	//copy the asset files locally (if needed, based on their .manifest files)
	get_files_to_copy( $localAssetCacheDirectory, $attrNames, $cacheFileObjectNames, $yetiAttrNames, $sourceFiles, $destFiles );
	gather_source_file_copies( $localAssetCacheDirectory, $sourceFiles, $gatheredSourceFiles );
	
	if( currentRenderer() == "redshift")
	{
		get_redshift_cache_files( $sourceFiles, $redshiftCacheFiles );
		gather_source_file_copies( $localAssetCacheDirectory, $redshiftCacheFiles, $gatheredSourceFiles );
	}
	
	//change the asset paths in the scene to point at the ones in the cache folder
	repath_assets( $localAssetCacheDirectory, $attrNames, $cacheFileObjectNames, $yetiAttrNames );

	//does a cleanup pass through the local asset cahce directory, checks their manifests, and deletes them if they're too old.
	delete_old_assets( $localAssetCacheDirectory, $slaveLACDaysToDelete ); 
}
```