141 lines
6.5 KiB
PowerShell
141 lines
6.5 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$ServerInstance,
|
|
[Parameter(Mandatory = $true)][string]$ObjectBackup,
|
|
[Parameter(Mandatory = $true)][string]$DatabaseSuffix,
|
|
[Parameter(Mandatory = $true)][string]$RequestFile,
|
|
[Parameter(Mandatory = $true)][string]$OutputFile
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$safeSuffix = ($DatabaseSuffix -replace '[^A-Za-z0-9_]', '_')
|
|
if ($safeSuffix.Length -gt 32) { $safeSuffix = $safeSuffix.Substring(0, 32) }
|
|
$database = 'HealerMan_RW_Objects_' + $safeSuffix
|
|
|
|
function Quote-SqlLiteral([string]$Value) { return "N'" + $Value.Replace("'", "''") + "'" }
|
|
function Quote-SqlIdentifier([string]$Value) { return '[' + $Value.Replace(']', ']]') + ']' }
|
|
|
|
function Invoke-SqlTable([System.Data.SqlClient.SqlConnection]$Connection, [string]$CommandText) {
|
|
$command = $Connection.CreateCommand()
|
|
$command.CommandTimeout = 300
|
|
$command.CommandText = $CommandText
|
|
$table = [System.Data.DataTable]::new()
|
|
$reader = $command.ExecuteReader()
|
|
try { $table.Load($reader) }
|
|
finally { $reader.Dispose(); $command.Dispose() }
|
|
return ,$table
|
|
}
|
|
|
|
function Invoke-SqlNonQuery([System.Data.SqlClient.SqlConnection]$Connection, [string]$CommandText) {
|
|
$command = $Connection.CreateCommand()
|
|
$command.CommandTimeout = 600
|
|
$command.CommandText = $CommandText
|
|
try { [void]$command.ExecuteNonQuery() }
|
|
finally { $command.Dispose() }
|
|
}
|
|
|
|
function Convert-DataRow([System.Data.DataRow]$Row) {
|
|
$result = [ordered]@{}
|
|
foreach ($column in $Row.Table.Columns) {
|
|
$value = $Row[$column.ColumnName]
|
|
$result[$column.ColumnName] = if ($value -is [DBNull]) { $null } else { $value }
|
|
}
|
|
return $result
|
|
}
|
|
|
|
function Remove-ForensicDatabase([System.Data.SqlClient.SqlConnection]$Connection, [string]$DatabaseName) {
|
|
$identifier = Quote-SqlIdentifier $DatabaseName
|
|
Invoke-SqlNonQuery $Connection @"
|
|
IF DB_ID($(Quote-SqlLiteral $DatabaseName)) IS NOT NULL
|
|
BEGIN
|
|
ALTER DATABASE $identifier SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
|
DROP DATABASE $identifier;
|
|
END
|
|
"@
|
|
}
|
|
|
|
function Restore-ForensicDatabase(
|
|
[System.Data.SqlClient.SqlConnection]$Connection,
|
|
[string]$DatabaseName,
|
|
[string]$BackupFile,
|
|
[string]$DataDirectory,
|
|
[string]$LogDirectory
|
|
) {
|
|
if (-not (Test-Path -LiteralPath $BackupFile -PathType Leaf)) { throw "Backup not found: $BackupFile" }
|
|
$files = Invoke-SqlTable $Connection ('RESTORE FILELISTONLY FROM DISK = ' + (Quote-SqlLiteral $BackupFile))
|
|
$moves = [System.Collections.Generic.List[string]]::new()
|
|
$dataIndex = 0
|
|
$logIndex = 0
|
|
foreach ($file in $files.Rows) {
|
|
if ([string]$file.Type -eq 'L') {
|
|
$physical = Join-Path $LogDirectory ($DatabaseName + '_' + $logIndex + '.ldf')
|
|
$logIndex += 1
|
|
} else {
|
|
$extension = if ($dataIndex -eq 0) { '.mdf' } else { '.ndf' }
|
|
$physical = Join-Path $DataDirectory ($DatabaseName + '_' + $dataIndex + $extension)
|
|
$dataIndex += 1
|
|
}
|
|
$moves.Add('MOVE ' + (Quote-SqlLiteral ([string]$file.LogicalName)) + ' TO ' + (Quote-SqlLiteral $physical))
|
|
}
|
|
Remove-ForensicDatabase $Connection $DatabaseName
|
|
$restoreSql = @(
|
|
'RESTORE DATABASE ' + (Quote-SqlIdentifier $DatabaseName),
|
|
'FROM DISK = ' + (Quote-SqlLiteral $BackupFile),
|
|
'WITH REPLACE, RECOVERY, STATS = 10,',
|
|
($moves -join (',' + [Environment]::NewLine))
|
|
) -join [Environment]::NewLine
|
|
Invoke-SqlNonQuery $Connection $restoreSql
|
|
Invoke-SqlNonQuery $Connection ('ALTER DATABASE ' + (Quote-SqlIdentifier $DatabaseName) + ' SET READ_ONLY WITH ROLLBACK IMMEDIATE')
|
|
}
|
|
|
|
$request = Get-Content -Raw -LiteralPath $RequestFile | ConvertFrom-Json
|
|
$imageIds = @($request.imageIds | ForEach-Object { [int]$_ } | Where-Object { $_ -gt 0 } | Sort-Object -Unique)
|
|
if (-not $imageIds.Count) { throw 'Paperdoll request contains no positive image IDs.' }
|
|
$idList = $imageIds -join ','
|
|
$connectionString = 'Server=' + $ServerInstance + ';Database=master;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True;Connect Timeout=15;Application Name=HealerMan RuneWaker Paperdoll Forensics'
|
|
$connection = [System.Data.SqlClient.SqlConnection]::new($connectionString)
|
|
try {
|
|
$connection.Open()
|
|
$paths = Invoke-SqlTable $connection @"
|
|
SELECT CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(4000)) AS DataPath,
|
|
CAST(SERVERPROPERTY('InstanceDefaultLogPath') AS nvarchar(4000)) AS LogPath,
|
|
(SELECT TOP (1) physical_name FROM sys.master_files WHERE database_id = 1 AND file_id = 1) AS MasterFile;
|
|
"@
|
|
$dataDirectory = [string]$paths.Rows[0].DataPath
|
|
$logDirectory = [string]$paths.Rows[0].LogPath
|
|
if ([string]::IsNullOrWhiteSpace($dataDirectory)) { $dataDirectory = Split-Path -Parent ([string]$paths.Rows[0].MasterFile) }
|
|
if ([string]::IsNullOrWhiteSpace($logDirectory)) { $logDirectory = $dataDirectory }
|
|
Restore-ForensicDatabase $connection $database $ObjectBackup $dataDirectory $logDirectory
|
|
$objectId = Quote-SqlIdentifier $database
|
|
$table = Invoke-SqlTable $connection @"
|
|
SELECT guid,imageid,actworld,skincolor,haircolor,
|
|
paperdollfacename,paperdollhairname,paperdollheadname,paperdollshoudername,paperdollclothesname,
|
|
paperdollglovesname,paperdollbeltname,paperdollpantsname,paperdollshoesname,paperdollbackname,
|
|
paperdollfacemaincolor,paperdollfaceoffcolor,paperdollhairmaincolor,paperdollhairoffcolor,
|
|
paperdollheadmaincolor,paperdollheadoffcolor,paperdollshoudermaincolor,paperdollshouderoffcolor,
|
|
paperdollclothesmaincolor,paperdollclothesoffcolor,paperdollglovesmaincolor,paperdollglovesoffcolor,
|
|
paperdollbeltmaincolor,paperdollbeltoffcolor,paperdollpantsmaincolor,paperdollpantsoffcolor,
|
|
paperdollshoesmaincolor,paperdollshoesoffcolor,paperdollbackmaincolor,paperdollbackoffcolor
|
|
FROM $objectId.dbo.ImageObjectDB
|
|
WHERE guid IN ($idList) OR imageid IN ($idList)
|
|
ORDER BY guid;
|
|
"@
|
|
$payload = [ordered]@{
|
|
schemaVersion = 1
|
|
queryVersion = 1
|
|
requestedImageIds = $imageIds
|
|
rows = @($table.Rows | ForEach-Object { Convert-DataRow $_ })
|
|
}
|
|
$directory = Split-Path -Parent $OutputFile
|
|
if ($directory) { [void](New-Item -ItemType Directory -Path $directory -Force) }
|
|
$payload | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $OutputFile -Encoding UTF8
|
|
Write-Host ('Exported {0} candidate ImageObjectDB rows.' -f $table.Rows.Count)
|
|
}
|
|
finally {
|
|
if ($connection.State -eq [System.Data.ConnectionState]::Open) {
|
|
Remove-ForensicDatabase $connection $database
|
|
$connection.Close()
|
|
}
|
|
$connection.Dispose()
|
|
}
|