How to move an asset to a newly created directory in the AssetDatabase ?

Hello community

I’m developing an asset postprocessor script that monitors the creation of text files containing dialog lines. When such a text file is created in (or moved to) specific places in the project hirerarchy a companion dialog asset is also created (using the AssetPostprocessor::OnPostprocessAllAssets callback). This companion asset is created in a subfolder and contains a parsed and structured version of the text file’s contents.

When the source text file is deleted or moved, its companion asset must be moved in another folder (either to a subfolder of the text file’s new location or a special orphans fodler). When the destination folder does not exist I create it before moving the asset.

My problem is that when I create a new asset, I can create the folder (using AssetDatabase::CreateFolder) and then create the asset (ScriptableObject::Instantiate + AssetDatabase::CreateAsset) and everything works fine. But when I try to move an existing asset to a new folder (AssetDatabase::CreateFolder + AssetDatabase::MoveAsset), the file moving fails ! The returned error message is as follows : Parent directory is not in asset database - So my question is : how can I move an existing asset to a newly created folder ?

I found [one old page from last year][1] related to this issue, but it was never marked as solved. Nevertheless I tried to apply the proposed solution using EditorApplication.delayedCall (which seems to have been replaced with [EditorApplication.update][2] since then) and put my asset moving operations there (while the folder creations stayed in OnPostprocessAllAssets to be performed before). The asset moving operation still fails, although this time no error message is returned.

Here is the code I’m using to do that (things that are relevant to my project only were stripped for clarity) :

static private void OnPostprocessAllAssets (
		string[] importedAssets,
		string[] deletedAssets,
		string[] movedAssets,
		string[] movedFromAssetPaths)
	{ 
    // The processing of created and deleted asset is stripped in this snippet.
  
    // This is the stripped version of the asset moving code.
		for (int i=0; i<movedAssets.Length; i++) {
			string newTextfilePath = movedAssets*;*

_ string oldTextfilePath = movedFromAssetPaths*;_
_
if (SomeConditionOnPaths()) {_
_
// TextfilePathToDialogPath computes a subfolder name where the dialog asset to move must go._
_
string newDialogPath = TextfilePathToDialogPath(newTextfilePath, false);_
_
Util.CreateDirectories(newDialogPath, false);_
_
string res = AssetDatabase.ValidateMoveAsset(oldAssetPath, newDialogPath);*_

* if (res.IsNullOrEmpty()) {*
* AssetDatabase.MoveAsset(oldAssetPath, newDialogPath);*
* } else {*
* GDebug.LogError(string.Format(“Unable to move a dialog asset between directories from ‘{0}’ to ‘{1}’: {2}.”, oldAssetPath, newDialogPath, res));*
* }*
}
}
* }*

// The rest of the class is stripped

}
_[1]: http://answers.unity3d.com/questions/598987/how-to-make-a-new-path-register-in-the-assetdataba.html*_
_
[2]: http://docs.unity3d.com/ScriptReference/EditorApplication-update.html*_

I’ll be answering my own question here, but after sleeping a bit on the problem a solution came to my mind today. The problem is apparently that when I create a new folder using AssetDatabase::CreateFolder, the folder is not available right away in the database. Instead it’s queued for a later import.

So what I do now is to try moving my asset as I did before, but if it fails, I put the move operation on hold for a while, by storing the source and destination paths in a list somewhere. Then in my OnPostProcessAllAssets callback, on monitor not only the assets I’m actually interested in processing, but also the folders that I created in previous calls. When the folder I created before is actually imported and gets through OnPostprocessAllAssets, I finally perform the pending moves which involve it. Here’s how it looks :

static private List<KeyValuePair<string, string>> delayedMoves = new List<KeyValuePair<string, string>>();

static private void OnPostprocessAllAssets (
	string[] importedAssets,
	string[] deletedAssets,
	string[] movedAssets,
	string[] movedFromAssetPaths)
{
	// The processing of deleted asset is stripped in this snippet.

	foreach (string newPath in importedAssets) {
		// The part related to delayed move operations
		if (delayedMoves.Count > 0) {
			MaybePerformDelayedMoves(newPath);
		}
		// The part related to what I really want to do to assets
		if (IsValidTextfilePath(newPath)) {
			newOrUpdatedTextfiles.Add(newPath);
		}
	}

	// This is the stripped version of the asset moving code.
	for (int i=0; i<movedAssets.Length; i++) {
		string newTextfilePath = movedAssets*;*

_ string oldTextfilePath = movedFromAssetPaths*;_
_
if (SomeConditionOnPaths()) {_
_
// TextfilePathToDialogPath computes a subfolder name where the dialog asset to move must go._
_
string newDialogPath = TextfilePathToDialogPath(newTextfilePath, false);_
_
Util.CreateDirectories(newDialogPath, false);_
_
// Nicely ask the database for permission to perform the move right away*_
* string res = AssetDatabase.ValidateMoveAsset(oldAssetPath, newDialogPath);*

* if (res.IsNullOrEmpty()) {*
* // It’s okay, we can move right away*
* AssetDatabase.MoveAsset(oldAssetPath, newDialogPath);*
* } else {*
* // A move fail may be a temporary error, if the destination directory*
* // was just created and is not imported yet. Try later.*
* MoveAssetLater(oldAssetPath, newDialogPath);*
* }*
* }*
* }*
}

static private void MoveAssetLater(string from, string to) {
* delayedMoves.Add(new KeyValuePair<string, string>(from, to));*
}

static private void MaybePerformDelayedMoves(string newAssetName) {
* int nMoves = delayedMoves.Count;*

* // The list is parsed in reverse order to remove elements easily.*
* for (int i = nMoves-1; i>=0; i–) {*
_ KeyValuePair<string, string> dm = delayedMoves*;
string destinationAssetPath = dm.Value;
string destinationFolder = Util.DirectoryFromPath(destinationAssetPath);
if(destinationFolder == newAssetName) {
// Perform move operation and delete origin directory if it is empty.
string originAssetPath = dm.Key;
string res = AssetDatabase.ValidateMoveAsset(originAssetPath, destinationAssetPath);
if (res.IsNullOrEmpty()) {
string originFolder = Util.DirectoryFromPath(originAssetPath);
AssetDatabase.MoveAsset(originAssetPath, destinationAssetPath);
} else {
GDebug.LogError(string.Format(“Unable to move {0} to {1} even after waiting for the destination folder to be imported.”, originAssetPath, destinationAssetPath));
}
delayedMoves.RemoveAt(i);
}
}
}*_

// The rest of the class is stripped
}

Had the same problem.
I believe found a more simple solution - just create the directory in OnPreprocess* call. It will be ready for use in OnPostprocessAllAssets .

Six years later and it’s still the same problem…