Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save larssteenhoff/819d634a920b4296959490345a8948d1 to your computer and use it in GitHub Desktop.
Save larssteenhoff/819d634a920b4296959490345a8948d1 to your computer and use it in GitHub Desktop.
Extract a .unitypackage to any directory (even outside the project folder) from within Unity
#define STOP_EXTRACTION_WHEN_WINDOW_CLOSED
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Text;
using System.Threading;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
public class UnitypackageExtractor : EditorWindow
{
#region Helper Classes
// Allows fetching the progress of GZipStream's decompression
// Credit: https://stackoverflow.com/a/413352/2373034
private class ProgressedFileStream : FileStream
{
private readonly UnitypackageExtractor progressTracker;
public ProgressedFileStream( UnitypackageExtractor progressTracker, string path ) : base( path, FileMode.Open, FileAccess.Read )
{
this.progressTracker = progressTracker;
}
public override int Read( byte[] buffer, int offset, int count )
{
int bytesRead = base.Read( buffer, offset, count );
progressTracker.decompressionProgressCurrent += bytesRead;
progressTracker.needsRepaint = true;
return bytesRead;
}
}
// Displays contents of a unitypackage in a tree view
private class AssetTreeView : TreeView
{
private class AssetData
{
public AssetTreeElement asset;
public string label; // Label will include asset's filesize, as well
public AssetData( AssetTreeElement asset, string label )
{
this.asset = asset;
this.label = label;
}
}
private readonly AssetTreeElement[] assets;
private readonly Dictionary<int, AssetData> idToAssetLookup = new Dictionary<int, AssetData>( 512 );
private readonly string trimmedDirectoryPath;
private readonly GUIContent sharedGUIContent = new GUIContent();
private string m_totalExtractFileSizeLabel;
public string TotalExtractFileSizeLabel { get { return m_totalExtractFileSizeLabel; } }
protected override bool CanMultiSelect( TreeViewItem item ) { return false; }
protected override bool CanRename( TreeViewItem item ) { return false; }
protected override void RenameEnded( RenameEndedArgs args ) { }
public AssetTreeView( TreeViewState treeViewState, AssetTreeElement[] assets, string trimmedDirectoryPath ) : base( treeViewState )
{
this.assets = assets;
this.trimmedDirectoryPath = trimmedDirectoryPath != null ? ( trimmedDirectoryPath + "/" ) : null;
showBorder = true;
#if UNITY_2019_3_OR_NEWER
depthIndentWidth += 5f;
#endif
// Draw only the visible rows. This requires setting useScrollView to false because we are using an external scroll view: https://docs.unity3d.com/ScriptReference/IMGUI.Controls.TreeView-useScrollView.html
#if UNITY_2018_2_OR_NEWER
useScrollView = false;
#else
object treeViewController = typeof( TreeView ).GetField( "m_TreeView", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).GetValue( this );
treeViewController.GetType().GetMethod( "SetUseScrollView", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).Invoke( treeViewController, new object[1] { false } );
#endif
Reload();
}
protected override TreeViewItem BuildRoot()
{
Dictionary<string, AssetTreeElement> folderAssets = new Dictionary<string, AssetTreeElement>( 64 );
Dictionary<string, TreeViewItem> directories = new Dictionary<string, TreeViewItem>( 64 );
TreeViewItem root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };
int id = 1;
for( int i = 0; i < assets.Length; i++ )
{
AssetTreeElement asset = assets[i];
if( asset.isFolder )
folderAssets[asset.path] = asset;
}
for( int i = 0; i < assets.Length; i++ )
{
AssetTreeElement asset = assets[i];
if( asset.isFolder )
continue;
// Create TreeViewItem for asset
idToAssetLookup[id] = new AssetData( asset, string.Concat( asset.filename, " (", EditorUtility.FormatBytes( asset.fileSize ), ")" ) );
TreeViewItem assetTreeItem = new TreeViewItem( id++, 0, asset.filename );
// Create TreeViewItem for asset's parent directory
string directoryPath = Path.GetDirectoryName( asset.path ).Replace( '\\', '/' );
TreeViewItem prevDirectory = null;
bool directoryRegisteredBefore = false;
while( !string.IsNullOrEmpty( directoryPath ) )
{
TreeViewItem directory;
if( directories.TryGetValue( directoryPath, out directory ) )
directoryRegisteredBefore = true;
else
{
AssetTreeElement folderAsset;
if( !folderAssets.TryGetValue( directoryPath, out folderAsset ) )
{
// unitypackage doesn't have a meta file for this folder, create a dummy folder holder
folderAsset = new AssetTreeElement( directoryPath, null ) { isFolder = true };
folderAssets[directoryPath] = folderAsset;
}
idToAssetLookup[id] = new AssetData( folderAsset, "" ); // Folder labels will be calculated in CalculateTotalExtractFileSizeDownwards
directory = new TreeViewItem( id++, 0, folderAsset.filename );
directories[directoryPath] = directory;
}
// Don't include trimmed directories in the TreeView
bool validDirectory = trimmedDirectoryPath == null || !StartsWithFast( trimmedDirectoryPath, directoryPath + "/" );
if( validDirectory )
{
if( prevDirectory != null )
directory.AddChild( prevDirectory );
else
directory.AddChild( assetTreeItem );
prevDirectory = directory;
}
if( directoryRegisteredBefore )
{
// If this directory is trimmed (i.e. validDirectory=false), set directoryRegisteredBefore to
// false so that "else if( !directoryRegisteredBefore )" condition below is executed and
// no valid directories are excluded from the TreeView
directoryRegisteredBefore = directoryRegisteredBefore && validDirectory;
break;
}
directoryPath = Path.GetDirectoryName( directoryPath ).Replace( '\\', '/' );
}
if( prevDirectory == null )
root.AddChild( assetTreeItem );
else if( !directoryRegisteredBefore )
root.AddChild( prevDirectory );
}
// Refresh folders' extract properties
if( root.hasChildren )
{
List<TreeViewItem> rootContents = root.children;
for( int i = 0; i < rootContents.Count; i++ )
RefreshExtractDownwards( rootContents[i] );
}
RecalculateTotalExtractFileSize( root );
SetupDepthsFromParentsAndChildren( root );
return root;
}
public HashSet<string> GetAllowedAssetGUIDs()
{
HashSet<string> result = new HashSet<string>();
bool everythingExtracted = true;
for( int i = 0; i < assets.Length; i++ )
{
if( assets[i].extract == AssetTreeElement.Extract.No )
everythingExtracted = false;
else if( !string.IsNullOrEmpty( assets[i].guid ) )
result.Add( assets[i].guid );
}
return everythingExtracted ? null : result; // We don't use HashSet when everything is extracted to improve performance
}
protected override void RowGUI( RowGUIArgs args )
{
AssetData assetData = idToAssetLookup[args.item.id];
AssetTreeElement asset = assetData.asset;
sharedGUIContent.text = assetData.label;
sharedGUIContent.image = asset.isFolder ? AssetDatabase.GetCachedIcon( "Assets" ) as Texture2D : UnityEditorInternal.InternalEditorUtility.GetIconForFile( asset.path );
Rect rect = args.rowRect;
rect.xMin += GetContentIndent( args.item );
if( asset.isFolder && asset.extract == AssetTreeElement.Extract.Mixed )
EditorGUI.showMixedValue = true;
EditorGUI.BeginChangeCheck();
bool shouldExtract = EditorGUI.Toggle( new Rect( rect.x, rect.y, rect.height, rect.height ), asset.extract == AssetTreeElement.Extract.Yes );
if( EditorGUI.EndChangeCheck() )
{
asset.extract = shouldExtract ? AssetTreeElement.Extract.Yes : AssetTreeElement.Extract.No;
if( asset.isFolder )
SetExtractDownwards( args.item, shouldExtract );
if( args.item.parent.id > 0 )
RefreshExtractUpwards( args.item.parent );
RecalculateTotalExtractFileSize( rootItem );
}
EditorGUI.showMixedValue = false;
rect.xMin += rect.height;
rect.y -= 2f;
rect.height += 4f; // Incrementing height fixes cropped icon issue on Unity 2019.2 or earlier
GUI.Label( rect, sharedGUIContent );
}
public void SetExtractAll( bool extract )
{
SetExtractDownwards( rootItem, extract );
RecalculateTotalExtractFileSize( rootItem );
}
private void SetExtractDownwards( TreeViewItem folder, bool extract )
{
if( folder.hasChildren )
{
List<TreeViewItem> folderContents = folder.children;
for( int i = 0; i < folderContents.Count; i++ )
{
AssetTreeElement folderContent = idToAssetLookup[folderContents[i].id].asset;
folderContent.extract = extract ? AssetTreeElement.Extract.Yes : AssetTreeElement.Extract.No;
if( folderContent.isFolder )
SetExtractDownwards( folderContents[i], extract );
}
}
}
private void RefreshExtractUpwards( TreeViewItem folder )
{
AssetTreeElement _folder = idToAssetLookup[folder.id].asset;
List<TreeViewItem> folderContents = folder.children;
AssetTreeElement.Extract extractMode = AssetTreeElement.Extract.Yes;
for( int i = 0; i < folderContents.Count; i++ )
{
AssetTreeElement folderContent = idToAssetLookup[folderContents[i].id].asset;
if( i == 0 )
extractMode = folderContent.extract;
else if( folderContent.extract != extractMode )
{
extractMode = AssetTreeElement.Extract.Mixed;
break;
}
}
_folder.extract = extractMode;
if( folder.parent.id > 0 )
RefreshExtractUpwards( folder.parent );
}
// Returns the total size of files in the folder
private long RefreshExtractDownwards( TreeViewItem folder )
{
if( !folder.hasChildren )
return 0L;
AssetTreeElement _folder = idToAssetLookup[folder.id].asset;
List<TreeViewItem> folderContents = folder.children;
// Calculate total size of files in the folder
_folder.fileSize = 0L;
AssetTreeElement.Extract extractMode = AssetTreeElement.Extract.Yes;
for( int i = 0; i < folderContents.Count; i++ )
{
AssetTreeElement folderContent = idToAssetLookup[folderContents[i].id].asset;
if( folderContent.isFolder )
_folder.fileSize += RefreshExtractDownwards( folderContents[i] );
else
_folder.fileSize += folderContent.fileSize;
if( i == 0 )
extractMode = folderContent.extract;
else if( folderContent.extract != extractMode )
extractMode = AssetTreeElement.Extract.Mixed;
}
_folder.extract = extractMode;
return _folder.fileSize;
}
// Calculates total size of files that will be extracted
private void RecalculateTotalExtractFileSize( TreeViewItem root )
{
long totalExtractFileSize = 0L, totalSize = 0L;
if( root.hasChildren )
{
List<TreeViewItem> rootContents = root.children;
for( int i = 0; i < rootContents.Count; i++ )
{
totalExtractFileSize += CalculateTotalExtractFileSizeDownwards( rootContents[i] );
totalSize += idToAssetLookup[rootContents[i].id].asset.fileSize;
}
}
m_totalExtractFileSizeLabel = string.Concat( "Total size: ", EditorUtility.FormatBytes( totalExtractFileSize ), " / ", EditorUtility.FormatBytes( totalSize ) );
}
// Returns the total size of extracted files in the folder
private long CalculateTotalExtractFileSizeDownwards( TreeViewItem folder )
{
AssetData folderData = idToAssetLookup[folder.id];
AssetTreeElement _folder = folderData.asset;
long totalExtractFileSize = 0L;
if( folder.hasChildren )
{
List<TreeViewItem> folderContents = folder.children;
for( int i = 0; i < folderContents.Count; i++ )
{
AssetTreeElement folderContent = idToAssetLookup[folderContents[i].id].asset;
if( folderContent.isFolder )
totalExtractFileSize += CalculateTotalExtractFileSizeDownwards( folderContents[i] );
else if( folderContent.extract == AssetTreeElement.Extract.Yes )
totalExtractFileSize += folderContent.fileSize;
}
}
// Folder labels should look like this: "SomeFolder (1.2 KB / 2.0 MB)"
folderData.label = string.Concat( _folder.filename, " (", EditorUtility.FormatBytes( totalExtractFileSize ), " / ", EditorUtility.FormatBytes( _folder.fileSize ), ")" );
return totalExtractFileSize;
}
}
[Serializable]
private class AssetTreeElement
{
public enum Extract { Yes = 0, No = 1, Mixed = 2 };
public string path;
public string guid;
public string filename;
public long fileSize;
public Extract extract = Extract.Yes;
public bool isFolder = false;
public AssetTreeElement( string path, string guid )
{
this.path = path;
this.guid = guid;
this.filename = Path.GetFileName( path );
}
}
#endregion
private const int BUFFER_SIZE = 4096; // Must be at least 512
private const string TRIMMED_DIRECTORY_NONE = "NONE";
private string packagePath, outputPath;
private bool isWorking, isDecompressing, isCalculatingAssetTree, needsRepaint;
private long decompressionProgressCurrent, decompressionProgressTotal;
private int renameProgressCurrent, renameProgressTotal;
private string[] trimmableDirectories;
private int trimmedDirectoryIndex = -1;
private string trimmedDirectoryPath = TRIMMED_DIRECTORY_NONE;
private bool assetDatabaseLocked;
private bool assemblyReloadLockedHint;
private Thread runningThread;
private bool abortThread;
private AssetTreeElement[] assetTree;
private AssetTreeView assetTreeView;
private TreeViewState assetTreeViewState;
private SearchField searchField;
private Vector2 scrollPos;
private readonly GUIContent pickFolderButtonContent = new GUIContent( "o", "Click to pick a folder. Right click to open Asset Store cache" );
private readonly GUIContent generateAssetTreeButtonContent = new GUIContent( "Select Assets to Extract...", "Select which assets to export from the unitypackage" );
[MenuItem( "Window/Unitypackage Extractor" )]
private static void Init()
{
UnitypackageExtractor window = GetWindow<UnitypackageExtractor>();
window.titleContent = new GUIContent( "Extractor" );
window.minSize = new Vector2( 300f, 100f );
window.Show();
}
private void OnEnable()
{
GenerateAssetTreeView();
}
#if STOP_EXTRACTION_WHEN_WINDOW_CLOSED
private void OnDestroy()
{
abortThread = true;
}
#endif
private void Update()
{
if( needsRepaint )
{
needsRepaint = false;
Repaint();
}
}
private void OnGUI()
{
scrollPos = GUILayout.BeginScrollView( scrollPos );
GUI.enabled = !isWorking;
float labelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = 120f;
string newPackagePath = PathField( ".unitypackage Path: ", packagePath, false );
if( newPackagePath != packagePath )
{
packagePath = newPackagePath;
trimmableDirectories = null;
trimmedDirectoryIndex = -1;
trimmedDirectoryPath = TRIMMED_DIRECTORY_NONE;
assetTree = null;
assetTreeView = null;
assetTreeViewState = null;
searchField = null;
}
outputPath = PathField( "Output Path: ", outputPath, true );
EditorGUIUtility.labelWidth = labelWidth;
if( assetTreeView != null )
{
if( trimmableDirectories != null && trimmableDirectories.Length > 0 )
{
GUILayout.BeginVertical( GUI.skin.box );
GUILayout.Label( "Trim Prefix: " + trimmedDirectoryPath, EditorStyles.boldLabel );
GUILayout.BeginHorizontal();
if( GUILayout.Toggle( trimmedDirectoryIndex == -1, "NONE", EditorStyles.toolbarButton ) && trimmedDirectoryIndex != -1 )
{
trimmedDirectoryIndex = -1;
trimmedDirectoryPath = TRIMMED_DIRECTORY_NONE;
GenerateAssetTreeView();
}
for( int i = 0; i < trimmableDirectories.Length; i++ )
{
if( GUILayout.Toggle( trimmedDirectoryIndex == i, trimmableDirectories[i], EditorStyles.toolbarButton ) && trimmedDirectoryIndex != i )
{
trimmedDirectoryIndex = i;
trimmedDirectoryPath = trimmableDirectories[0];
for( int j = 1; j <= i; j++ )
trimmedDirectoryPath += "/" + trimmableDirectories[j];
GenerateAssetTreeView();
}
}
GUILayout.EndHorizontal();
GUILayout.EndVertical();
}
GUILayout.BeginHorizontal();
GUILayout.Label( "All:" );
if( GUILayout.Button( "Select" ) )
assetTreeView.SetExtractAll( true );
if( GUILayout.Button( "Deselect" ) )
assetTreeView.SetExtractAll( false );
if( GUILayout.Button( "Expand" ) )
assetTreeView.ExpandAll();
if( GUILayout.Button( "Collapse" ) )
assetTreeView.CollapseAll();
GUILayout.EndHorizontal();
GUILayout.Space( 3f );
assetTreeView.searchString = searchField.OnGUI( assetTreeView.searchString );
assetTreeView.OnGUI( EditorGUILayout.GetControlRect( false, assetTreeView.totalHeight ) );
GUILayout.Label( assetTreeView.TotalExtractFileSizeLabel );
EditorGUILayout.Space();
}
GUI.enabled = true;
if( !isWorking )
{
if( ( assetTree == null || assetTree.Length == 0 ) && GUILayout.Button( generateAssetTreeButtonContent ) )
{
packagePath = packagePath.Trim();
if( string.IsNullOrEmpty( packagePath ) || !File.Exists( packagePath ) )
{
Debug.LogError( ".unitypackage doesn't exist at: " + packagePath );
return;
}
ExecuteOnSeparateThread( true, false );
}
if( GUILayout.Button( "Extract!" ) )
{
packagePath = packagePath.Trim();
outputPath = outputPath.Trim();
if( outputPath.Length > 0 && ( outputPath[outputPath.Length - 1] == '/' || outputPath[outputPath.Length - 1] == '\\' ) )
outputPath = outputPath.Substring( 0, outputPath.Length - 1 );
if( string.IsNullOrEmpty( packagePath ) || !File.Exists( packagePath ) )
{
Debug.LogError( ".unitypackage doesn't exist at: " + packagePath );
return;
}
if( string.IsNullOrEmpty( outputPath ) )
{
Debug.LogError( "Output Path can't be blank" );
return;
}
if( !Directory.Exists( outputPath ) )
Directory.CreateDirectory( outputPath );
else if( Directory.GetFileSystemEntries( outputPath ).Length > 0 && !EditorUtility.DisplayDialog( "Warning", "Output Path isn't an empty folder. Conflicting files will be overridden. Proceed?", "OK", "Cancel" ) )
return;
// If we are extracting assets to this Unity project, lock AssetDatabase for safety reasons
// Assets are considered to be extracted to this Unity project if:
// a) outputPath points to the root of the Unity project directory
// b) outputPath points to either Assets folder or a subfolder of it
// In other words, if we extract the assets to PROJECT_PATH/Library or PROJECT_PATH/SomeCustomFolder, we don't have to lock AssetDatabase
string projectAbsolutePath = Path.GetFullPath( Directory.GetCurrentDirectory() ) + "/";
string outputAbsolutePath = Path.GetFullPath( outputPath ) + "/";
bool lockAssetDatabase = projectAbsolutePath == outputAbsolutePath || StartsWithFast( outputAbsolutePath, Path.GetFullPath( "Assets" ) );
if( lockAssetDatabase && !EditorUtility.DisplayDialog( "Warning", "Unitypackage will be extracted to this Unity project. To avoid any potential issues, you shouldn't modify your scenes, assets or scripts until the extraction is completed. Proceed?", "OK", "Cancel" ) )
return;
ExecuteOnSeparateThread( false, lockAssetDatabase );
}
}
else if( GUILayout.Button( "Stop" ) )
abortThread = true;
if( isWorking )
{
Rect progressbarRect = EditorGUILayout.GetControlRect( false, EditorGUIUtility.singleLineHeight );
if( isDecompressing )
{
if( decompressionProgressTotal > 0L )
EditorGUI.ProgressBar( progressbarRect, (float) ( (double) decompressionProgressCurrent / decompressionProgressTotal ), isCalculatingAssetTree ? "Generating Asset Tree" : "Decompressing Archive" );
}
else if( renameProgressTotal > 0 )
EditorGUI.ProgressBar( progressbarRect, (float) renameProgressCurrent / renameProgressTotal, string.Concat( "Renaming Assets: ", renameProgressCurrent, "/", renameProgressTotal ) );
}
GUILayout.Space( 5f );
GUILayout.EndScrollView();
}
private void GenerateAssetTreeView()
{
if( assetTree != null && assetTree.Length > 0 )
{
bool shouldExpandAll = false;
if( assetTreeViewState == null )
{
assetTreeViewState = new TreeViewState();
shouldExpandAll = true;
}
assetTreeView = new AssetTreeView( assetTreeViewState, assetTree, trimmedDirectoryIndex >= 0 ? trimmedDirectoryPath : null );
searchField = new SearchField();
searchField.downOrUpArrowKeyPressed += assetTreeView.SetFocusAndEnsureSelectedItem;
if( shouldExpandAll )
assetTreeView.ExpandAll();
}
}
private string PathField( string label, string path, bool isDirectory )
{
GUILayout.BeginHorizontal();
path = EditorGUILayout.TextField( label, path );
if( GUILayout.Button( pickFolderButtonContent, GUILayout.Width( 25f ) ) )
{
string initialPath = "";
try
{
if( Event.current.button == 1 )
{
initialPath = Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData );
if( !string.IsNullOrEmpty( initialPath ) )
initialPath = Path.Combine( initialPath, "Unity/Asset Store-5.x" );
if( !Directory.Exists( initialPath ) )
initialPath = "";
}
if( string.IsNullOrEmpty( initialPath ) && !string.IsNullOrEmpty( path ) )
{
if( Directory.Exists( path ) )
initialPath = path;
else
{
string parentPath = Path.GetDirectoryName( path );
if( !string.IsNullOrEmpty( parentPath ) && Directory.Exists( parentPath ) )
initialPath = parentPath;
}
}
}
catch
{
initialPath = "";
}
string selectedPath;
if( isDirectory )
selectedPath = EditorUtility.OpenFolderPanel( "Choose output directory", initialPath, "" );
else
selectedPath = EditorUtility.OpenFilePanel( "Choose .unitypackage", initialPath, "unitypackage" );
if( !string.IsNullOrEmpty( selectedPath ) )
path = selectedPath;
GUIUtility.keyboardControl = 0; // Remove focus from active text field
}
GUILayout.EndHorizontal();
return path;
}
private void ExecuteOnSeparateThread( bool isCalculatingAssetTree, bool lockAssetDatabase )
{
abortThread = false;
isDecompressing = true;
this.isCalculatingAssetTree = isCalculatingAssetTree;
if( lockAssetDatabase )
{
AssetDatabase.StartAssetEditing();
assetDatabaseLocked = true;
}
else
assetDatabaseLocked = false;
try
{
runningThread = isCalculatingAssetTree ? new Thread( GenerateAssetTree ) : new Thread( ExtractAssets );
runningThread.Start();
scrollPos.y = 99999f; // Scroll to the bottom to see the progressbar
assemblyReloadLockedHint = false;
isWorking = true;
EditorApplication.LockReloadAssemblies();
EditorApplication.update -= UnlockAssembliesAfterExtraction;
EditorApplication.update += UnlockAssembliesAfterExtraction;
}
catch
{
if( assetDatabaseLocked )
{
AssetDatabase.StopAssetEditing();
assetDatabaseLocked = false;
}
throw;
}
}
private void UnlockAssembliesAfterExtraction()
{
if( !isWorking )
{
try
{
EditorApplication.update -= UnlockAssembliesAfterExtraction;
EditorApplication.UnlockReloadAssemblies();
if( isCalculatingAssetTree )
GenerateAssetTreeView();
}
finally
{
if( assetDatabaseLocked )
{
AssetDatabase.StopAssetEditing();
AssetDatabase.Refresh();
assetDatabaseLocked = false;
}
}
}
else
{
if( EditorApplication.isPlayingOrWillChangePlaymode )
{
EditorApplication.isPlaying = false;
Debug.LogWarning( "Can't enter Play mode while extracting a Unitypackage, <b>Stop</b> the operation first!" );
}
if( !assemblyReloadLockedHint && EditorApplication.isCompiling )
{
assemblyReloadLockedHint = true;
Debug.LogWarning( "Can't reload assemblies while extracting a Unitypackage, <b>Stop</b> the operation first!" );
}
}
}
private void GenerateAssetTree()
{
try
{
AssetTreeElement[] assetTree;
string packagePath = this.packagePath;
decompressionProgressCurrent = 0L;
decompressionProgressTotal = 0L;
using( ProgressedFileStream fs = new ProgressedFileStream( this, packagePath ) )
using( GZipStream gzip = new GZipStream( fs, CompressionMode.Decompress ) )
{
decompressionProgressTotal = fs.Length;
assetTree = GenerateAssetTreeFromTarGz( gzip );
isDecompressing = false;
}
assetTreeView = null;
searchField = null;
this.assetTree = assetTree;
if( abortThread )
Debug.Log( "<b>Stopped generating asset tree:</b> " + packagePath );
else if( assetTree != null && assetTree.Length > 0 )
{
// Calculate trimmableDirectories
string sharedParentDirectories = null;
for( int i = 0; i < assetTree.Length; i++ )
{
AssetTreeElement asset = assetTree[i];
if( asset.isFolder )
continue;
string directoryPath = Path.GetDirectoryName( asset.path ).Replace( '\\', '/' );
// Find common parent directories of the assets in order to populate trimmableDirectories
if( sharedParentDirectories == null )
sharedParentDirectories = directoryPath;
else if( sharedParentDirectories.Length > 0 )
{
int minLength = Mathf.Min( sharedParentDirectories.Length, directoryPath.Length );
int lastPathSeparatorIndex = 0, iterator = 0;
for( ; iterator < minLength; iterator++ )
{
char ch = sharedParentDirectories[iterator];
if( ch != directoryPath[iterator] )
break;
if( ch == '/' )
lastPathSeparatorIndex = iterator;
}
if( iterator == minLength )
lastPathSeparatorIndex = iterator;
sharedParentDirectories = sharedParentDirectories.Substring( 0, lastPathSeparatorIndex );
}
}
trimmableDirectories = sharedParentDirectories.Split( '/' );
}
}
catch( Exception e )
{
Debug.LogException( e );
}
isWorking = false;
needsRepaint = true;
}
private void ExtractAssets()
{
try
{
string[] extractedDirectories;
string packagePath = this.packagePath;
string outputPath = this.outputPath;
string trimmedDirectoryPathWithSeparator = trimmedDirectoryIndex >= 0 ? ( trimmedDirectoryPath + "/" ) : null;
decompressionProgressCurrent = 0L;
decompressionProgressTotal = 0L;
renameProgressCurrent = 0;
renameProgressTotal = 0;
using( ProgressedFileStream fs = new ProgressedFileStream( this, packagePath ) )
using( GZipStream gzip = new GZipStream( fs, CompressionMode.Decompress ) )
{
decompressionProgressTotal = fs.Length;
extractedDirectories = ExtractAssetsFromTarGz( gzip, outputPath, assetTreeView != null ? assetTreeView.GetAllowedAssetGUIDs() : null );
isDecompressing = false;
}
foreach( string directory in extractedDirectories )
{
if( abortThread )
break;
string pathnameFile = Path.Combine( directory, "pathname" );
if( !File.Exists( pathnameFile ) )
continue;
string path = File.ReadAllText( pathnameFile );
int newLineIndex = path.IndexOf( '\n' );
if( newLineIndex > 0 )
path = path.Substring( 0, newLineIndex );
path = path.Trim();
// Trim common prefix directories from path, if desired
if( trimmedDirectoryPathWithSeparator != null )
{
if( path.Length < trimmedDirectoryPathWithSeparator.Length && StartsWithFast( trimmedDirectoryPathWithSeparator, path + "/" ) )
continue;
else if( StartsWithFast( path, trimmedDirectoryPathWithSeparator ) )
path = path.Substring( trimmedDirectoryPathWithSeparator.Length );
}
path = Path.Combine( outputPath, path );
Directory.CreateDirectory( Path.GetDirectoryName( path ) );
string assetFile = Path.Combine( directory, "asset" );
if( File.Exists( assetFile ) )
{
if( File.Exists( path ) )
File.Copy( assetFile, path, true );
else
File.Move( assetFile, path );
}
else // This is a directory
Directory.CreateDirectory( path );
string assetMetaFile = Path.Combine( directory, "asset.meta" );
if( File.Exists( assetMetaFile ) )
{
string metaPath = path + ".meta";
if( File.Exists( metaPath ) )
File.Copy( assetMetaFile, metaPath, true );
else
File.Move( assetMetaFile, metaPath );
}
renameProgressCurrent++;
needsRepaint = true;
}
// Cleanup temporary directories
foreach( string directory in extractedDirectories )
Directory.Delete( directory, true );
if( abortThread )
Debug.Log( "<b>Stopped extracting:</b> " + packagePath );
else
Debug.Log( "<b>Finished extracting:</b> " + packagePath );
}
catch( Exception e )
{
Debug.LogException( e );
}
isWorking = false;
needsRepaint = true;
}
// Credit: https://gist.github.com/davetransom/553aeb3c4388c3eb448c0afe564cd2e3
private AssetTreeElement[] GenerateAssetTreeFromTarGz( GZipStream input )
{
List<AssetTreeElement> assetTree = new List<AssetTreeElement>( 512 );
Dictionary<string, long> assetTreeSizes = new Dictionary<string, long>( 512 );
byte[] buffer = new byte[BUFFER_SIZE];
while( true )
{
if( abortThread )
return new AssetTreeElement[0];
input.Read( buffer, 0, 512 );
string name = Encoding.ASCII.GetString( buffer, 0, 100 ).Trim( '\0' ).Trim();
if( name.Length == 0 )
break;
else if( StartsWithFast( name, "./" ) )
{
if( name.Length == 2 )
continue;
name = name.Substring( 2 );
}
long size = Convert.ToInt64( Encoding.ASCII.GetString( buffer, 124, 12 ).Trim( '\0', ' ' ), 8 );
if( size > 0 ) // This is a file entry
{
if( Path.GetFileName( name ) == "pathname" )
{
// Fetch asset's path
using( MemoryStream memoryStream = new MemoryStream( 128 ) )
{
long remaining = size;
int bytesRead;
while( ( bytesRead = input.Read( buffer, 0, remaining >= BUFFER_SIZE ? BUFFER_SIZE : (int) remaining ) ) > 0 )
{
memoryStream.Write( buffer, 0, bytesRead );
remaining -= bytesRead;
}
string assetPath = Encoding.UTF8.GetString( memoryStream.ToArray() );
int newLineIndex = assetPath.IndexOf( '\n' );
if( newLineIndex > 0 )
assetPath = assetPath.Substring( 0, newLineIndex );
assetTree.Add( new AssetTreeElement( assetPath, Path.GetDirectoryName( name ) ) );
}
}
else
{
if( EndsWithFast( name, "/asset" ) )
assetTreeSizes[Path.GetDirectoryName( name )] = size;
// Skip the file contents
long remaining = size;
int bytesRead;
while( ( bytesRead = input.Read( buffer, 0, remaining >= BUFFER_SIZE ? BUFFER_SIZE : (int) remaining ) ) > 0 )
remaining -= bytesRead;
}
}
int offset = 512 - (int) ( size % 512 );
if( offset > 0 && offset < 512 )
input.Read( buffer, 0, offset );
}
// Mark folders inside assetTree
HashSet<string> directoriesInAssetTree = new HashSet<string>();
for( int i = 0; i < assetTree.Count; i++ )
{
long assetSize;
if( assetTreeSizes.TryGetValue( assetTree[i].guid, out assetSize ) )
assetTree[i].fileSize = assetSize;
string directoryPath = Path.GetDirectoryName( assetTree[i].path ).Replace( '\\', '/' );
while( !string.IsNullOrEmpty( directoryPath ) )
{
if( directoriesInAssetTree.Contains( directoryPath ) )
break;
// Check if this directory's path is equal to an asset's path in assetTree
for( int j = assetTree.Count - 1; j >= 0; j-- )
{
if( assetTree[j].path == directoryPath )
assetTree[j].isFolder = true;
}
directoriesInAssetTree.Add( directoryPath );
directoryPath = Path.GetDirectoryName( directoryPath ).Replace( '\\', '/' );
}
}
assetTree.Sort( ( a1, a2 ) => a1.path.CompareTo( a2.path ) );
return assetTree.ToArray();
}
// Credit: https://gist.github.com/davetransom/553aeb3c4388c3eb448c0afe564cd2e3
private string[] ExtractAssetsFromTarGz( GZipStream input, string outputDir, HashSet<string> allowedAssetGUIDs )
{
List<string> extractedDirectories = new List<string>( 64 );
byte[] buffer = new byte[BUFFER_SIZE];
while( true )
{
if( abortThread )
return new string[0];
input.Read( buffer, 0, 512 );
string name = Encoding.ASCII.GetString( buffer, 0, 100 ).Trim( '\0' ).Trim();
if( name.Length == 0 )
break;
else if( StartsWithFast( name, "./" ) )
{
if( name.Length == 2 )
continue;
name = name.Substring( 2 );
}
long size = Convert.ToInt64( Encoding.ASCII.GetString( buffer, 124, 12 ).Trim( '\0', ' ' ), 8 );
if( size > 0 ) // This is a file entry
{
// ".icon.png": no need to extract unitypackage's thumbnail
if( ( allowedAssetGUIDs == null || allowedAssetGUIDs.Contains( Path.GetDirectoryName( name ) ) ) && name != ".icon.png" )
{
// Extract the file
string output = Path.Combine( outputDir, name );
Directory.CreateDirectory( Path.GetDirectoryName( output ) );
using( FileStream fs = File.Open( output, FileMode.OpenOrCreate, FileAccess.Write ) )
{
long remaining = size;
int bytesRead;
while( ( bytesRead = input.Read( buffer, 0, remaining >= BUFFER_SIZE ? BUFFER_SIZE : (int) remaining ) ) > 0 )
{
fs.Write( buffer, 0, bytesRead );
remaining -= bytesRead;
}
}
}
else
{
// Skip the file contents
long remaining = size;
int bytesRead;
while( ( bytesRead = input.Read( buffer, 0, remaining >= BUFFER_SIZE ? BUFFER_SIZE : (int) remaining ) ) > 0 )
remaining -= bytesRead;
}
}
else // This is a directory entry
{
if( name.Length > 1 && name[name.Length - 1] == '/' )
name = name.Substring( 0, name.Length - 1 );
if( allowedAssetGUIDs == null || allowedAssetGUIDs.Contains( name ) )
{
// After extraction, contents of each directory will be renamed to match the target asset's name
renameProgressTotal++;
extractedDirectories.Add( Path.Combine( outputDir, name ) );
}
}
int offset = 512 - (int) ( size % 512 );
if( offset > 0 && offset < 512 )
input.Read( buffer, 0, offset );
}
for( int i = extractedDirectories.Count - 1; i >= 0; i-- )
{
if( !Directory.Exists( extractedDirectories[i] ) )
extractedDirectories.RemoveAt( i );
}
return extractedDirectories.ToArray();
}
// Returns true is str starts with prefix
private static bool StartsWithFast( string str, string prefix )
{
int aLen = str.Length;
int bLen = prefix.Length;
int ap = 0; int bp = 0;
while( ap < aLen && bp < bLen && str[ap] == prefix[bp] )
{
ap++;
bp++;
}
return bp == bLen;
}
// Returns true is str ends with postfix
private static bool EndsWithFast( string str, string postfix )
{
int aLen = str.Length - 1;
int bLen = postfix.Length - 1;
while( aLen >= 0 && bLen >= 0 && str[aLen] == postfix[bLen] )
{
aLen--;
bLen--;
}
return bLen < 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment