Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Extract a .unitypackage to any directory (even outside the project folder) from within Unity
//#define OPEN_ASSET_STORE_CACHE_AS_INITIAL_PATH
#define STOP_EXTRACTION_WHEN_WINDOW_CLOSED
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading;
using UnityEditor;
using UnityEngine;
public class UnitypackageExtractor : EditorWindow
{
private string packagePath, outputPath;
private bool isWorking, isDecompressing, needsRepaint;
private int extractionProgressCurrent, extractionProgressTotal;
private long decompressionProgressCurrent, decompressionProgressTotal;
private bool assemblyReloadLockedHint;
private Thread runningThread;
private bool abortThread;
[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();
}
#if STOP_EXTRACTION_WHEN_WINDOW_CLOSED
private void OnDestroy()
{
abortThread = true;
}
#endif
private void Update()
{
if( needsRepaint )
{
needsRepaint = false;
Repaint();
}
}
private void OnGUI()
{
packagePath = PathField( ".unitypackage Path: ", packagePath, false );
outputPath = PathField( "Output Path: ", outputPath, true );
if( !isWorking )
{
if( GUILayout.Button( "Extract!" ) )
{
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 )
{
Debug.LogError( "Output Path must be an empty folder" );
return;
}
abortThread = false;
isDecompressing = true;
runningThread = new Thread( Execute );
runningThread.Start();
assemblyReloadLockedHint = false;
isWorking = true;
EditorApplication.LockReloadAssemblies();
EditorApplication.update -= UnlockAssembliesAfterExtraction;
EditorApplication.update += UnlockAssembliesAfterExtraction;
}
}
else if( GUILayout.Button( "Stop" ) )
abortThread = true;
Rect progressbarRect = EditorGUILayout.GetControlRect( false, EditorGUIUtility.singleLineHeight );
if( isWorking )
{
if( isDecompressing )
{
if( decompressionProgressTotal > 0L )
EditorGUI.ProgressBar( progressbarRect, (float) ( (double) decompressionProgressCurrent / decompressionProgressTotal ), "Decompressing Archive" );
}
else if( extractionProgressTotal > 0 )
EditorGUI.ProgressBar( progressbarRect, (float) extractionProgressCurrent / extractionProgressTotal, string.Concat( "Extracting Assets: ", extractionProgressCurrent, "/", extractionProgressTotal ) );
}
}
private void UnlockAssembliesAfterExtraction()
{
if( !isWorking )
{
EditorApplication.update -= UnlockAssembliesAfterExtraction;
EditorApplication.UnlockReloadAssemblies();
}
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 string PathField( string label, string path, bool isDirectory )
{
GUILayout.BeginHorizontal();
path = EditorGUILayout.TextField( label, path );
if( GUILayout.Button( "o", GUILayout.Width( 25f ) ) )
{
string selectedPath;
if( isDirectory )
selectedPath = EditorUtility.OpenFolderPanel( "Choose output directory", "", "" );
else
{
#if OPEN_ASSET_STORE_CACHE_AS_INITIAL_PATH
string initialPath = Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData );
if( !string.IsNullOrEmpty( initialPath ) )
initialPath = Path.Combine( initialPath, "Unity/Asset Store-5.x" );
if( !Directory.Exists( initialPath ) )
initialPath = "";
#else
string initialPath = "";
#endif
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 Execute()
{
try
{
string packagePath = this.packagePath;
string outputPath = this.outputPath;
using( FileStream fs = new FileStream( packagePath, FileMode.Open, FileAccess.Read ) )
{
decompressionProgressTotal = fs.Length;
ExtractTarGz( fs, outputPath );
}
foreach( string directory in Directory.GetDirectories( outputPath ) )
{
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.Combine( outputPath, path.Trim() );
Directory.CreateDirectory( Path.GetDirectoryName( path ) );
string assetFile = Path.Combine( directory, "asset" );
if( File.Exists( assetFile ) )
File.Move( assetFile, path );
else // This is a directory
Directory.CreateDirectory( path );
string assetMetaFile = Path.Combine( directory, "asset.meta" );
if( File.Exists( assetMetaFile ) )
File.Move( assetMetaFile, path + ".meta" );
Directory.Delete( directory, true );
extractionProgressCurrent++;
needsRepaint = true;
}
if( !abortThread )
Debug.Log( "<b>Finished extracting:</b> " + packagePath );
else
Debug.Log( "<b>Stopped extracting:</b> " + packagePath );
}
catch( Exception e )
{
Debug.LogException( e );
}
isWorking = false;
}
// Credit: https://gist.github.com/davetransom/553aeb3c4388c3eb448c0afe564cd2e3
private void ExtractTarGz( Stream stream, string outputDir )
{
// A GZipStream is not seekable, so copy it to a MemoryStream first
using( GZipStream gzip = new GZipStream( stream, CompressionMode.Decompress ) )
{
const int chunk = 4096;
using( MemoryStream ms = new MemoryStream() )
{
int read;
byte[] buffer = new byte[chunk];
do
{
if( abortThread )
return;
read = gzip.Read( buffer, 0, chunk );
ms.Write( buffer, 0, read );
decompressionProgressCurrent += read;
needsRepaint = true;
}
while( read == chunk );
extractionProgressCurrent = 0;
extractionProgressTotal = 0;
// Count number of files in the archive first
ms.Seek( 0, SeekOrigin.Begin );
ExtractTar( ms, outputDir, true );
// Extract the files afterwards
isDecompressing = false;
ms.Seek( 0, SeekOrigin.Begin );
ExtractTar( ms, outputDir, false );
}
}
}
// Credit: https://gist.github.com/davetransom/553aeb3c4388c3eb448c0afe564cd2e3
private void ExtractTar( Stream stream, string outputDir, bool isCountingFiles )
{
byte[] buffer = new byte[100];
while( true )
{
if( abortThread )
return;
stream.Read( buffer, 0, 100 );
string name = Encoding.ASCII.GetString( buffer ).Trim( '\0', ' ' );
if( name != null )
name = name.Trim();
if( string.IsNullOrEmpty( name ) )
break;
stream.Seek( 24, SeekOrigin.Current );
stream.Read( buffer, 0, 12 );
long size;
string hex = Encoding.ASCII.GetString( buffer, 0, 12 ).Trim( '\0', ' ' );
try
{
size = Convert.ToInt64( hex, 8 );
}
catch( Exception ex )
{
throw new Exception( "Could not parse hex: " + hex, ex );
}
stream.Seek( 376L, SeekOrigin.Current );
string output = Path.Combine( outputDir, name );
if( size > 0 ) // Ignores directory entries
{
if( isCountingFiles )
{
stream.Seek( size, SeekOrigin.Current );
extractionProgressTotal++;
}
else
{
Directory.CreateDirectory( Path.GetDirectoryName( output ) );
using( FileStream fs = File.Open( output, FileMode.OpenOrCreate, FileAccess.Write ) )
{
byte[] blob = new byte[size];
stream.Read( blob, 0, blob.Length );
fs.Write( blob, 0, blob.Length );
}
extractionProgressCurrent++;
needsRepaint = true;
}
}
else if( isCountingFiles )
extractionProgressTotal++; // Each directory will be processed after being extracted
long offset = 512 - ( stream.Position % 512 );
if( offset == 512 )
offset = 0;
stream.Seek( offset, SeekOrigin.Current );
}
}
}
@yasirkula

This comment has been minimized.

Copy link
Owner Author

@yasirkula yasirkula commented Mar 17, 2020

How To

Simply create a folder called Editor inside your Project window and add this script inside it. Then, open Window-Unitypackage Extractor, configure the parameters and hit "Extract!". The operation will be handled by a separate thread, so it won't block Unity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.