PowerShell script exchange
AndrewAdvnetsol
Service Provider
Posts: 34
Liked: 4 times
Joined: Jan 24, 2020 6:06 pm
Full Name: Andrew Carmichael
Contact:

Re: Get tape content via powershell

Post by AndrewAdvnetsol »

JTT wrote: โ†‘Oct 23, 2023 1:40 pm Hello

Is there a solution or a script, to find out, what backup is written to what tape?
If i open the Tape infra view, select some offline or online tape from the view and then use the Files view, it shows what backup and from what Repo it was written, but how can i get it for all the tapes?
@JTT

I use the following scripts to pull that info. You can try them and see if they work for you. Hopefully they help.

This is a script I named Tape Name Script:

Code: Select all

$ReportDir = 'C:\Scripts\Veeam\Reports'
$date = Get-Date -format "yyyyMMdd-HHmmss"
$backups = get-vbrtapebackup | where {$_.VMCount -ne 0}
$rps = Get-VBRRestorePoint -Backup $backups
$results = foreach ($rp in $rps) {
  $dbOib = [Veeam.Backup.DBManager.CDBManager]::Instance.TapeOibs.GetMediaTapeNamesByOibs($rp.Id)
  $barcode = $dbOib.Values
  $rp | select Name, creationtime, @{n='TapeMedium';e={$barcode}}
  }
$results | export-csv $ReportDir\tape_$Date.csv
Write-Host "Tape inventory has been exported to: "
Write-Host "$ReportDir\tape_$Date.csv"
This is a script I named Tape Inventory Script: (note you will have to modify this with the hostname and SQL Instance Name of your system)

Code: Select all

$VeeamSqlServer = 'hostname\SQLInstanceName' 
$ScriptDir = 'C:\Scripts\Veeam'
$ReportDir = 'C:\Scripts\Veeam\Reports'
#Import-module "sqlps"
$date = Get-Date -format "yyyyMMdd-HHmmss"
$TapeInventory = Invoke-Sqlcmd -InputFile $ScriptDir\VeeamTapeInventory.sql -ServerInstance "$VeeamSQLServer"
$TapeInventory | Export-Csv $ReportDir\VeeamTapeInventory_$Date.csv -NoTypeInformation
Write-Host "Tape inventory has been exported to: "
Write-Host "$ReportDir\VeeamTapeInventory_$Date.csv"
You will also need this info saved in a file called VeeamTapeInventory.sql. (I just pasted it into a txt file then renamed file in file explorer to change the .txt to .sql)

Code: Select all

USE VeeamBackup;

    WITH PathInfo AS
    (
     SELECT  [Id]
    ,Parent_Id
    ,Name
    ,FolderPath = CONVERT(NVARCHAR(800), name)
       FROM [dbo].[Tape.directories]
      WHERE Parent_Id IS NULL
      UNION ALL
     SELECT  TempTD.Id
    ,TempTD.Parent_Id
    ,TempTD.name
    ,FolderPath = CONVERT(NVARCHAR(800), cte.FolderPath+'\'+TempTD.name)
       FROM [dbo].[Tape.directories] TempTD
       JOIN PathInfo cte ON cte.Id = TempTD.Parent_Id
    )

SELECT
TTM_Name AS Name,

--TH_Name AS Backup_Server,
Folder_Path,
TF_Name AS File_Name,
TFP_Incompletion AS FileSegmentNumber, 
File_Size_GB,
--Tape_Capacity_GB,
Tape_Remaining_GB,
--TTM_Protected AS IsTapeProtected,
TTM_Media_Time AS Media_Time,
/*CASE WHEN 
Tape_Physical_Location IS NULL THEN 'Offline'
ELSE Tape_Physical_Location
END AS Tape_Physical_Location,*/
--TB_Name AS Tape_Backup_Job,
TBS_Name AS Tape_Backup_Set,
TBS_ExpirationDate AS Tape_Backup_Set_Expiration,
TTM_LastWriteTime AS Last_Write_Time
--TTM_Description AS Tape_Description,
--TMP_Name AS Tape_Media_Pool,
--TMP_Description AS Tape_Media_Pool_Description

FROM
(SELECT TFV.file_id AS TFV_FileID,
TFV.backup_set_id AS TFV_BackupSetID,
TFV.id AS TFV_ID,
CAST(TFV.Size / 1073741824.0E AS DECIMAL(10, 2)) AS File_Size_GB,
TF.directory_id AS TF_DirectoryID,
TF.name AS TF_Name,
TFP.media_sequence_number AS TFP_MediaSequenceNumber,
TFP.id AS TFP_ID,
TFP.file_version_id AS TFP_FileVersionID,
TFP.incompletion AS TFP_Incompletion,
TH.name AS TH_Name,
PathInfo.folderpath AS Folder_Path
     FROM [Tape.file_versions] AS TFV
LEFT JOIN [dbo].[Tape.file_parts] TFP  
ON TFV.id = TFP.file_version_id
LEFT JOIN [Tape.files] TF 
ON TFV.file_id = TF.id
LEFT JOIN [Tape.directories] TD 
ON TF.directory_id = TD.id
LEFT JOIN [Tape.hosts] TH 
ON TD.host_id = TH.id
INNER JOIN PathInfo
ON PathInfo.id = TD.id
) AS FileParts
  RIGHT JOIN 
(SELECT TTM.id AS TTM_ID,
TTM.name as TTM_name,
TTM.media_time as TTM_media_time,
TTM.media_sequence_number AS TTM_MediaSequenceNumber,
TTM.location_address AS TTM_LocationAddress,
TTM.Last_Write_Time AS TTM_LastWriteTime,
TTM.Description AS TTM_Description,
CASE TTM.Protected
WHEN '0' THEN 'No'
WHEN '1' THEN 'Yes'
ELSE 'Other'
END AS TTM_Protected,
TTMBS.tape_medium_id AS TTMBS_TapeMediumID,
TTMBS.backup_set_id AS TTMBS_BackupSetID,
TBS.id AS TBS_ID,
TBS.name AS TBS_Name,
TBS.backup_id AS TBS_BackupID,
TBS.expiration_date AS TBS_ExpirationDate,
TB.name AS TB_Name,
TMV.description AS TMV_Description,
TMV.name AS TMV_Name,
CAST(TTM.Capacity / 1073741824.0E AS DECIMAL(10, 2)) AS Tape_Capacity_GB,
CAST(TTM.Remaining / 1073741824.0E AS DECIMAL(10, 2)) AS Tape_Remaining_GB,
TL.Name AS TL_Name,
TL.id AS TL_ID,
TL.tape_server_id AS TL_TapeServerID,
TTM.Location_type AS TTM_LocationType,
CASE TTM.Location_Type
WHEN '0' THEN TL.Name + ' - Tape Drive'
WHEN '1' THEN TL.Name + ' - Slot ' + CAST((TTM.Location_Address + 1) AS NVARCHAR(255))
WHEN '2' THEN 'Tape Vault - ' + TMV.Name
ELSE 'Other'
END AS Tape_Physical_Location,
TMP.name AS TMP_Name,
TMP.Description AS TMP_Description  
FROM [Tape.tape_mediums] AS TTM
LEFT JOIN [dbo].[Tape.tape_medium_backup_sets] TTMBS  
ON TTM.id = TTMBS.tape_medium_id
LEFT JOIN  [dbo].[Tape.backup_sets] TBS 
ON TTMBS.backup_set_id = TBS.id
LEFT JOIN [Tape.backups] TB 
ON TBS.backup_id = TB.id
LEFT JOIN [Tape.media_in_vaults] TMIV
ON TTM.id = TMIV.media_id
LEFT JOIN [Tape.media_vaults] TMV
ON TMIV.vault_id = TMV.id
LEFT JOIN [Tape.libraries] TL
ON TTM.location_library_id = TL.id
INNER JOIN [Tape.media_pools] TMP
ON media_pool_id = TMP.id
) AS BackupSets
ON BackupSets.TBS_ID = FileParts.TFV_BackupSetID
AND BackupSets.TTM_MediaSequenceNumber = FileParts.TFP_MediaSequenceNumber

WHERE NOT (NOT (BackupSets.TBS_ID IS NULL) AND (TF_Name IS NULL))

ORDER BY TTM_name ASC
roland
Enthusiast
Posts: 29
Liked: 5 times
Joined: Oct 25, 2010 4:15 pm
Contact:

Re: Get tape content via powershell

Post by roland »

Hi there,

I'm wondering if the actual Veeam PowerShell module is capable of providing us with tape content directly without using "unsupported features".
My main goal is to list every files (vbk, vib, and others) stored on a specif tape even if this file is made with "file to tape job " and not a "backup to tape job"....
Thanks
david.domask
Product Manager
Posts: 3623
Liked: 878 times
Joined: Jun 28, 2016 12:12 pm
Contact:

Re: Get tape content via powershell

Post by david.domask »

Hi roland,

Right now you will need to use the unsupported workarounds found in this topic; the request for official cmdlets/api endpoints for this is known, but for now use the workarounds in this topic. Thanks!
David Domask | Product Management: Principal Analyst
alexanderjohn2
Lurker
Posts: 2
Liked: never
Joined: Apr 23, 2026 3:35 pm
Full Name: Alexander John
Contact:

Re: Get tape content via powershell

Post by alexanderjohn2 »

Hello, we are on V12 and are looking for roughly the functionality as requested. I need to find a way to get the contents as shown here in the screenshot exported into a csv or something like it

https://helpcenter.veeam.com/docs/vbr/u ... tml?ver=13

So far I did not succeed in accessing the data there in any way. What is currently the "best" way to do it?

Thanks for any pointers, Alexander
david.domask
Product Manager
Posts: 3623
Liked: 878 times
Joined: Jun 28, 2016 12:12 pm
Contact:

Re: Get tape content via powershell

Post by david.domask »

Hi Alexander, welcome to the forums.

Currently you will need to utilize one of the workarounds posted in this topic. I believe that the solution provided by user winnt is still valid for v12, but I have not checked it in awhile, but I recall it was quite complete.
David Domask | Product Management: Principal Analyst
alexanderjohn2
Lurker
Posts: 2
Liked: never
Joined: Apr 23, 2026 3:35 pm
Full Name: Alexander John
Contact:

Re: Get tape content via powershell

Post by alexanderjohn2 »

Hello David, thanks for your reply.
Do you know if the "other way around" is possible and we can get the tape that a particular file is stored on?

Find-VBRTapeCatalogItem sounds correct but we cannot get a result that make sense to us.

Thanks again, Alexander
david.domask
Product Manager
Posts: 3623
Liked: 878 times
Joined: Jun 28, 2016 12:12 pm
Contact:

Re: Get tape content via powershell

Post by david.domask »

Happy to have shared, Alexander.

Currently the only way I'm aware of that works reliably is the one I linked above.

Find-VBRTapeCatalogItem is meant for automating file restores from Tape, I've not played deeply with those cmdlets to see if there's a supported way to link to the needed tape or not prior to starting the restore but I do not see one checking the cmdlets briefly, so I would advise continue with the unsupported workaround here.
David Domask | Product Management: Principal Analyst
imaxiss
Lurker
Posts: 1
Liked: 1 time
Joined: May 05, 2026 11:35 am
Full Name: dijaga1954@justnapa.com
Contact:

Re: Get tape content via powershell

Post by imaxiss » 1 person likes this post

The sql method described on the second page still works.
The CSV format is not convenient for viewing, so I made an offline web page with tabular and hierarchical output that can be printed and attached to tape.
Maybe it will be useful to someone.

Code: Select all

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>Veeam Tape Inventory Explorer | Tape Backup Hierarchy</title>
    <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
            background: #f0f4f8;
            color: #1a2c3e;
            padding: 2rem 1.5rem;
        }

        .container {
            max-width: 1600px;
            margin: 0 auto;
        }

        .header {
            display: flex;
            flex-wrap: wrap;
            justify-content: space-between;
            align-items: flex-end;
            margin-bottom: 1.5rem;
            gap: 1rem;
        }

        h1 {
            font-size: 1.9rem;
            font-weight: 600;
            background: linear-gradient(135deg, #0b5e7a, #1c7e9c);
            background-clip: text;
            -webkit-background-clip: text;
            color: transparent;
            letter-spacing: -0.3px;
        }

        .sub {
            font-size: 0.85rem;
            color: #4a627a;
            margin-top: 0.3rem;
        }

        .file-loader {
            background: white;
            border-radius: 24px;
            padding: 1.2rem 1.8rem;
            margin-bottom: 1.8rem;
            border: 1px solid #dce5ec;
            box-shadow: 0 2px 8px rgba(0,0,0,0.04);
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            justify-content: space-between;
            gap: 1rem;
        }

        .file-info {
            display: flex;
            align-items: center;
            gap: 1rem;
            flex-wrap: wrap;
            font-size: 0.9rem;
        }

        .file-badge {
            background: #eef2f6;
            padding: 0.4rem 1rem;
            border-radius: 40px;
            font-family: monospace;
            font-size: 0.8rem;
            color: #1f6e8c;
        }

        .load-btn {
            background: #2c7da0;
            color: white;
            border: none;
            padding: 0.6rem 1.4rem;
            border-radius: 40px;
            font-weight: 500;
            font-size: 0.85rem;
            cursor: pointer;
            transition: 0.2s;
            display: inline-flex;
            align-items: center;
            gap: 8px;
        }

        .load-btn:hover {
            background: #1f5e7a;
            transform: scale(1.01);
        }

        .status-msg {
            font-size: 0.8rem;
            color: #54809b;
        }

        .stats-bar {
            background: white;
            border-radius: 24px;
            padding: 0.7rem 1.5rem;
            box-shadow: 0 2px 6px rgba(0,0,0,0.02);
            display: flex;
            flex-wrap: wrap;
            gap: 1.8rem;
            margin-bottom: 1.8rem;
            align-items: center;
            font-weight: 500;
            border: 1px solid #e2edf2;
        }

        .stat-item {
            display: flex;
            align-items: baseline;
            gap: 0.4rem;
            font-size: 0.9rem;
            color: #1f4e6e;
        }

        .stat-value {
            font-weight: 800;
            font-size: 1.3rem;
            color: #0f4c5f;
        }

        .controls {
            background: white;
            border-radius: 20px;
            padding: 1rem 1.5rem;
            margin-bottom: 1.8rem;
            display: flex;
            flex-wrap: wrap;
            gap: 1rem;
            align-items: center;
            justify-content: space-between;
            border: 1px solid #e2edf2;
        }

        .search-box {
            flex: 2;
            min-width: 220px;
            display: flex;
            align-items: center;
            background: #f8fafd;
            border-radius: 40px;
            padding: 0.4rem 1rem;
            border: 1px solid #cfdfe8;
        }

        .search-box i {
            color: #7c9eb3;
            margin-right: 8px;
        }

        .search-box input {
            border: none;
            background: transparent;
            width: 100%;
            font-size: 0.9rem;
            padding: 0.5rem 0;
            outline: none;
            font-family: inherit;
        }

        .filter-group {
            display: flex;
            flex-wrap: wrap;
            gap: 0.8rem;
            align-items: center;
        }

        .filter-select {
            background: #f8fafd;
            border: 1px solid #cfdfe8;
            border-radius: 30px;
            padding: 0.5rem 1rem;
            font-size: 0.85rem;
            font-family: inherit;
            cursor: pointer;
            outline: none;
        }

        .reset-btn, .print-btn, .view-toggle-btn {
            background: #eef3f7;
            border: none;
            padding: 0.5rem 1.1rem;
            border-radius: 32px;
            font-weight: 500;
            font-size: 0.8rem;
            cursor: pointer;
            transition: 0.2s;
            color: #1f5e7e;
            display: inline-flex;
            align-items: center;
            gap: 6px;
        }

        .reset-btn:hover, .print-btn:hover, .view-toggle-btn:hover {
            background: #e0eaf1;
        }

        .print-btn {
            background: #2c7da0;
            color: white;
        }

        .print-btn:hover {
            background: #1f5e7a;
        }

        .view-toggle-btn.active {
            background: #2c7da0;
            color: white;
        }

        .table-wrapper {
            background: white;
            border-radius: 20px;
            box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
            overflow-x: auto;
            border: 1px solid #e6edf2;
        }

        .data-table {
            width: 100%;
            border-collapse: collapse;
            font-size: 0.8rem;
            min-width: 1400px;
        }

        .data-table th {
            background: #eef3fa;
            padding: 1rem 0.8rem;
            text-align: left;
            font-weight: 600;
            color: #1f4662;
            border-bottom: 1px solid #d4e2ec;
            position: sticky;
            top: 0;
            background-color: #eef3fa;
            white-space: nowrap;
        }

        .data-table td {
            padding: 0.8rem;
            border-bottom: 1px solid #ecf3f8;
            color: #1e3b4f;
            vertical-align: top;
            word-break: break-word;
        }

        .data-table tr:hover td {
            background-color: #f9fdfe;
        }

        .tree-view {
            padding: 1rem;
        }

        .tree-node {
            margin-left: 1.5rem;
            list-style: none;
            font-size: 0.85rem;
        }

        .tree-node-root {
            margin-left: 0;
        }

        .tree-item {
            padding: 0.4rem 0;
            display: flex;
            align-items: flex-start;
            gap: 8px;
            flex-wrap: wrap;
            border-bottom: 1px solid #f0f2f5;
        }

        .tree-toggle {
            cursor: pointer;
            user-select: none;
            width: 20px;
            display: inline-flex;
            justify-content: center;
            font-weight: bold;
            color: #2c7da0;
        }

        .tree-label {
            display: flex;
            flex-wrap: wrap;
            gap: 12px;
            align-items: baseline;
            flex: 1;
        }

        .tree-label-icon {
            width: 24px;
            color: #5f8a9f;
        }

        .tree-details {
            display: flex;
            flex-wrap: wrap;
            gap: 16px;
            font-size: 0.75rem;
            color: #4a6f88;
        }

        .tree-details span {
            background: #f0f4f9;
            padding: 0.1rem 0.5rem;
            border-radius: 20px;
        }

        .children-container {
            padding-left: 1.8rem;
        }

        .badge {
            display: inline-block;
            padding: 0.2rem 0.6rem;
            border-radius: 40px;
            font-size: 0.7rem;
            font-weight: 600;
            text-align: center;
        }

        .badge-protected {
            background: #dff0e6;
            color: #1c6e43;
        }

        .badge-unprotected {
            background: #fee9e6;
            color: #bc4e2c;
        }

        .numeric {
            font-family: monospace;
            font-weight: 500;
        }

        .empty-row td {
            text-align: center;
            padding: 3rem;
            color: #7e9ab0;
            font-style: italic;
        }

        footer {
            margin-top: 1.5rem;
            text-align: center;
            font-size: 0.75rem;
            color: #6f8eaa;
        }

        .print-header {
            display: none;
            text-align: center;
            margin-bottom: 1rem;
            border-bottom: 2px solid #2c7da0;
            padding-bottom: 0.5rem;
        }
        .print-header h2 {
            font-size: 1.2rem;
            font-weight: 600;
            color: #1a2c3e;
        }
        .print-header p {
            font-size: 0.8rem;
            color: #4a627a;
            margin-top: 0.2rem;
        }

        @media print {
            body {
                background: white;
                padding: 0;
                margin: 0;
                font-size: 8pt !important;
            }
            .file-loader, .controls, .stats-bar, footer, .print-btn, .reset-btn, .load-btn, .view-toggle-btn {
                display: none !important;
            }
            #tableView, #treeView {
                display: none !important;
            }
            body.printing-table #tableView {
                display: block !important;
            }
            body.printing-tree #treeView {
                display: block !important;
            }
            .print-header {
                display: block !important;
            }
            .table-wrapper {
                box-shadow: none;
                border: none;
                overflow-x: visible !important;
                width: 100%;
                margin: 0;
                padding: 0;
                background: white;
            }
            .data-table {
                min-width: 100% !important;
                width: auto !important;
                font-size: 6.5pt !important;
                border-collapse: collapse;
                page-break-inside: avoid;
            }
            .data-table th, .data-table td {
                border: 1px solid #aaa !important;
                padding: 0.2rem 0.3rem !important;
                white-space: normal !important;
                word-break: break-word;
                vertical-align: top;
            }
            .data-table th {
                background-color: #eef3fa !important;
                -webkit-print-color-adjust: exact;
                print-color-adjust: exact;
                font-size: 6.5pt !important;
            }
            .badge {
                font-size: 5.5pt !important;
                padding: 0.1rem 0.3rem !important;
                white-space: nowrap;
            }
            .data-table tr {
                page-break-inside: avoid;
                break-inside: avoid;
            }
            .tree-view {
                display: block !important;
                overflow-x: visible;
                font-size: 7pt !important;
            }
            .tree-item, .tree-details {
                break-inside: avoid;
            }
            .tree-details span {
                font-size: 6pt !important;
                padding: 0.1rem 0.3rem;
            }
        }

        @media (max-width: 700px) {
            body {
                padding: 1rem;
            }
            .controls {
                flex-direction: column;
                align-items: stretch;
            }
            .file-loader {
                flex-direction: column;
                align-items: stretch;
            }
        }

        @page {
            size: landscape;
            margin: 1rem;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="print-header" id="printHeader">
        <h2><i class="fas fa-tape"></i> Veeam Tape Inventory Explorer</h2>
        <p id="printFileName">File: <span>not loaded</span></p>
        <p id="printTimestamp">Print date: <span></span></p>
    </div>

    <div class="header">
        <div>
            <h1><i class="fas fa-tape" style="color: #2c7da0;"></i> Veeam Tape Inventory Explorer</h1>
            <div class="sub">Tape backup management โ€” table / folder & file hierarchy</div>
        </div>
    </div>

    <div class="file-loader">
        <div class="file-info">
            <i class="fas fa-file-csv" style="font-size: 1.4rem; color: #2c7da0;"></i>
            <span><strong>Data source:</strong> </span>
            <span id="filePathDisplay" class="file-badge">No file selected</span>
            <span id="loadStatus" class="status-msg"><i class="fas fa-info-circle"></i> Click "Choose File" to load CSV</span>
        </div>
        <button id="chooseFileBtn" class="load-btn"><i class="fas fa-folder-open"></i> Choose File</button>
        <input type="file" id="fileInput" accept=".csv" style="display: none;" />
    </div>

    <div class="stats-bar" id="statsBar">
        <div class="stat-item"><span class="stat-value" id="totalRows">0</span> <span>records</span></div>
        <div class="stat-item"><i class="fas fa-hdd"></i> Total size: <span class="stat-value" id="totalSizeGB">0</span> GB</div>
        <div class="stat-item"><i class="fas fa-shield-alt"></i> Protected: <span class="stat-value" id="protectedCount">0</span></div>
        <div class="stat-item"><i class="fas fa-database"></i> Unique tapes: <span class="stat-value" id="uniqueTapes">0</span></div>
    </div>

    <div class="controls" id="controlsPanel" style="display: none;">
        <div class="search-box">
            <i class="fas fa-search"></i>
            <input type="text" id="searchInput" placeholder="Search all fields (file name, server, barcode, pool...)">
        </div>
        <div class="filter-group">
            <select id="filterProtected" class="filter-select">
                <option value="all">All media</option>
                <option value="true">Protected (true)</option>
                <option value="false">Unprotected (false)</option>
            </select>
            <select id="filterMediaPool" class="filter-select">
                <option value="all">All media pools</option>
            </select>
            <button id="resetFiltersBtn" class="reset-btn"><i class="fas fa-undo-alt"></i> Reset</button>
            <button id="toggleViewBtn" class="view-toggle-btn"><i class="fas fa-folder-tree"></i> Hierarchy</button>
            <button id="printPdfBtn" class="print-btn"><i class="fas fa-print"></i> Print / PDF</button>
        </div>
    </div>

    <div class="table-wrapper" id="tableWrapper">
        <div id="tableView" style="display: block;">
            <table class="data-table" id="dataTable">
                <thead>
                    <tr>
                        <th>BarcodeID</th><th>Backup Server</th><th>Folder Path</th><th>File Name</th><th>Segment #</th><th>Size (GB)</th>
                        <th>Tape Cap.(GB)</th><th>Tape Rem.(GB)</th><th>Protected</th><th>Tape Location</th><th>Backup Job</th>
                        <th>Backup Set</th><th>Set Expiration</th><th>Last Write</th><th>Tape Descr.</th><th>Media Pool</th><th>Pool Description</th>
                    </tr>
                </thead>
                <tbody id="tableBody">
                    <tr class="empty-row"><td colspan="17">๐Ÿ’ฟ Waiting for CSV file upload. Click "Choose File" and select Veeam report. ๐Ÿ“‚</td></tr>
                </tbody>
            </table>
        </div>
        <div id="treeView" style="display: none;" class="tree-view"></div>
    </div>
    <footer>
        <i class="fas fa-info-circle"></i> Supported columns: BarcodeID, Backup_Server, Folder_Path, File_Name, FileSegmentNumber, File_Size_GB, Tape_Capacity_GB, Tape_Remaining_GB, IsTapeProtected, Tape_Physical_Location, Tape_Backup_Job, Tape_Backup_Set, Tape_Backup_Set_Expiration, Last_Write_Time, Tape_Description, Tape_Media_Pool, Tape_Media_Pool_Description.
    </footer>
</div>

<script>
    let dataset = [];
    let currentView = 'table';
    let currentFileName = "No file selected";

    const tbody = document.getElementById('tableBody');
    const treeContainer = document.getElementById('treeView');
    const tableViewDiv = document.getElementById('tableView');
    const treeViewDiv = document.getElementById('treeView');
    const searchInput = document.getElementById('searchInput');
    const filterProtected = document.getElementById('filterProtected');
    const filterMediaPool = document.getElementById('filterMediaPool');
    const resetBtn = document.getElementById('resetFiltersBtn');
    const printBtn = document.getElementById('printPdfBtn');
    const toggleViewBtn = document.getElementById('toggleViewBtn');
    const totalRowsSpan = document.getElementById('totalRows');
    const totalSizeSpan = document.getElementById('totalSizeGB');
    const protectedCountSpan = document.getElementById('protectedCount');
    const uniqueTapesSpan = document.getElementById('uniqueTapes');
    const controlsPanel = document.getElementById('controlsPanel');
    const loadStatusSpan = document.getElementById('loadStatus');
    const filePathDisplaySpan = document.getElementById('filePathDisplay');
    const chooseFileBtn = document.getElementById('chooseFileBtn');
    const fileInput = document.getElementById('fileInput');
    const printFileNameSpan = document.getElementById('printFileName');
    const printTimestampSpan = document.getElementById('printTimestamp').querySelector('span');

    function updatePrintHeader() {
        if (printFileNameSpan) {
            printFileNameSpan.innerHTML = `File: ${currentFileName}`;
        }
        if (printTimestampSpan) {
            const now = new Date();
            const formatted = now.toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
            printTimestampSpan.textContent = formatted;
        }
    }

    function parseCSVToObjects(csvText) {
        const lines = csvText.split(/\r?\n/);
        if (lines.length < 2) return [];
        const firstLine = lines[0];
        let headers = parseCSVLine(firstLine);
        headers = headers.map(h => h.replace(/^\uFEFF/, '').trim());
        const records = [];
        for (let i = 1; i < lines.length; i++) {
            if (lines[i].trim() === '') continue;
            const values = parseCSVLine(lines[i]);
            if (values.length === 0) continue;
            const obj = {};
            headers.forEach((header, idx) => {
                let val = values[idx] !== undefined ? values[idx] : '';
                if (header === 'FileSegmentNumber') {
                    let num = parseInt(val, 10);
                    obj[header] = isNaN(num) ? 0 : num;
                } else if (header === 'File_Size_GB' || header === 'Tape_Capacity_GB' || header === 'Tape_Remaining_GB') {
                    let num = parseFloat(val);
                    obj[header] = isNaN(num) ? 0 : num;
                } else if (header === 'IsTapeProtected') {
                    let lowerVal = val.toString().toLowerCase();
                    obj[header] = (lowerVal === 'true' || lowerVal === '1' || lowerVal === 'yes');
                } else {
                    obj[header] = val;
                }
            });
            records.push(obj);
        }
        return records;
    }

    function parseCSVLine(line) {
        const result = [];
        let inQuotes = false;
        let current = '';
        for (let i = 0; i < line.length; i++) {
            const ch = line[i];
            if (ch === '"') {
                if (inQuotes && line[i+1] === '"') {
                    current += '"';
                    i++;
                } else {
                    inQuotes = !inQuotes;
                }
            } else if (ch === ',' && !inQuotes) {
                result.push(current.trim());
                current = '';
            } else {
                current += ch;
            }
        }
        result.push(current.trim());
        return result;
    }

    function updateMediaPoolFilter(data) {
        const pools = new Set();
        data.forEach(record => {
            const pool = record.Tape_Media_Pool;
            if (pool && pool.toString().trim() !== '') pools.add(pool.toString());
        });
        let options = '<option value="all">All media pools</option>';
        Array.from(pools).sort().forEach(p => {
            options += `<option value="${escapeHtml(p)}">${escapeHtml(p)}</option>`;
        });
        filterMediaPool.innerHTML = options;
    }

    function filterData() {
        const searchTerm = searchInput.value.trim().toLowerCase();
        const protectedFilter = filterProtected.value;
        const mediaPoolFilter = filterMediaPool.value;
        return dataset.filter(record => {
            if (protectedFilter !== 'all') {
                const isProtected = record.IsTapeProtected === true;
                if (protectedFilter === 'true' && !isProtected) return false;
                if (protectedFilter === 'false' && isProtected) return false;
            }
            if (mediaPoolFilter !== 'all') {
                const recordPool = record.Tape_Media_Pool ? record.Tape_Media_Pool.toString() : '';
                if (recordPool !== mediaPoolFilter) return false;
            }
            if (searchTerm !== '') {
                const searchableFields = [
                    record.BarcodeID, record.Backup_Server, record.Folder_Path, record.File_Name,
                    record.Tape_Physical_Location, record.Tape_Backup_Job, record.Tape_Backup_Set,
                    record.Tape_Backup_Set_Expiration, record.Last_Write_Time, record.Tape_Description,
                    record.Tape_Media_Pool, record.Tape_Media_Pool_Description,
                    (record.FileSegmentNumber?.toString() || ''), (record.File_Size_GB?.toString() || '')
                ];
                return searchableFields.some(field => field && field.toString().toLowerCase().includes(searchTerm));
            }
            return true;
        });
    }

    function escapeHtml(str) {
        if (str === undefined || str === null) return '';
        return String(str).replace(/[&<>]/g, function(m) {
            if (m === '&') return '&amp;';
            if (m === '<') return '&lt;';
            if (m === '>') return '&gt;';
            return m;
        });
    }

    function formatNumber(val, decimals = 1) {
        if (typeof val !== 'number' || isNaN(val)) return '0';
        return val.toFixed(decimals);
    }

    function formatBoolean(value) {
        if (value === true) return '<span class="badge badge-protected"><i class="fas fa-lock"></i> Protected</span>';
        return '<span class="badge badge-unprotected"><i class="fas fa-unlock-alt"></i> No</span>';
    }

    function truncate(str, maxLen) {
        if (!str) return '';
        if (str.length <= maxLen) return str;
        return str.substring(0, maxLen) + 'โ€ฆ';
    }

    function renderTable(records) {
        if (!records.length) {
            tbody.innerHTML = '<tr class="empty-row"><td colspan="17">๐Ÿ“ญ No data matching filters.</td></tr>';
            return;
        }
        let html = '';
        for (let rec of records) {
            html += `<tr>
                <td>${escapeHtml(rec.BarcodeID)}</td>
                <td>${escapeHtml(rec.Backup_Server)}</td>
                <td title="${escapeHtml(rec.Folder_Path)}">${escapeHtml(truncate(rec.Folder_Path, 60))}</td>
                <td>${escapeHtml(rec.File_Name)}</td>
                <td class="numeric">${rec.FileSegmentNumber || 0}</td>
                <td class="numeric">${formatNumber(rec.File_Size_GB, 1)}</td>
                <td class="numeric">${formatNumber(rec.Tape_Capacity_GB, 0)}</td>
                <td class="numeric">${formatNumber(rec.Tape_Remaining_GB, 1)}</td>
                <td>${formatBoolean(rec.IsTapeProtected)}</td>
                <td>${escapeHtml(rec.Tape_Physical_Location)}</td>
                <td>${escapeHtml(rec.Tape_Backup_Job)}</td>
                <td>${escapeHtml(rec.Tape_Backup_Set)}</td>
                <td>${escapeHtml(rec.Tape_Backup_Set_Expiration)}</td>
                <td>${escapeHtml(rec.Last_Write_Time)}</td>
                <td>${escapeHtml(rec.Tape_Description)}</td>
                <td>${escapeHtml(rec.Tape_Media_Pool)}</td>
                <td>${escapeHtml(rec.Tape_Media_Pool_Description)}</td>
            </tr>`;
        }
        tbody.innerHTML = html;
    }

    function buildHierarchy(records) {
        const tree = [];
        const serverMap = new Map();
        for (const rec of records) {
            const server = rec.Backup_Server || 'Unknown Server';
            if (!serverMap.has(server)) serverMap.set(server, []);
            serverMap.get(server).push(rec);
        }
        for (const [server, serverRecords] of serverMap.entries()) {
            const serverNode = { name: server, type: 'server', children: [], items: [] };
            const pathMap = new Map();
            for (const rec of serverRecords) {
                let folderPath = rec.Folder_Path || '/';
                if (!folderPath.startsWith('/')) folderPath = '/' + folderPath;
                if (!pathMap.has(folderPath)) pathMap.set(folderPath, []);
                pathMap.get(folderPath).push(rec);
            }
            for (const [path, pathRecords] of pathMap.entries()) {
                const pathParts = path.split('/').filter(p => p);
                let currentLevel = serverNode.children;
                for (let i = 0; i < pathParts.length; i++) {
                    const part = pathParts[i];
                    let existing = currentLevel.find(n => n.name === part && n.type === 'folder');
                    if (!existing) {
                        existing = { name: part, type: 'folder', children: [], items: [], fullPath: pathParts.slice(0,i+1).join('/') };
                        currentLevel.push(existing);
                    }
                    if (i === pathParts.length - 1) {
                        for (const rec of pathRecords) existing.items.push(rec);
                    }
                    currentLevel = existing.children;
                }
            }
            tree.push(serverNode);
        }
        return tree;
    }

    function renderTree(records) {
        if (!records.length) {
            treeContainer.innerHTML = '<div class="empty-row" style="padding:2rem;text-align:center;">๐Ÿ“ญ No data to display in hierarchy</div>';
            return;
        }
        const hierarchy = buildHierarchy(records);
        treeContainer.innerHTML = '<ul class="tree-node-root" style="list-style:none;padding-left:0;">' + renderTreeNodes(hierarchy) + '</ul>';
        document.querySelectorAll('.tree-toggle').forEach(toggle => {
            toggle.addEventListener('click', function(e) {
                const targetId = this.getAttribute('data-target');
                const childContainer = document.getElementById(targetId);
                if (childContainer) {
                    const isVisible = childContainer.style.display !== 'none';
                    childContainer.style.display = isVisible ? 'none' : 'block';
                    this.textContent = isVisible ? 'โ–ถ' : 'โ–ผ';
                }
                e.stopPropagation();
            });
        });
    }

    function renderTreeNodes(nodes) {
        let html = '';
        for (const node of nodes) {
            const nodeId = `tree_${Math.random().toString(36).substr(2, 8)}_${node.name.replace(/[^a-z0-9]/gi, '_')}`;
            if (node.type === 'server') {
                html += `<li class="tree-node">
                    <div class="tree-item">
                        <span class="tree-toggle" data-target="${nodeId}" style="cursor:pointer;">โ–ผ</span>
                        <i class="fas fa-server tree-label-icon" style="color:#2c7da0;"></i>
                        <span class="tree-label"><strong>${escapeHtml(node.name)}</strong></span>
                    </div>
                    <div id="${nodeId}" class="children-container">
                        ${node.children?.length ? '<ul class="tree-node" style="list-style:none;">' + renderTreeNodes(node.children) + '</ul>' : ''}
                        ${node.items?.length ? renderFileItems(node.items) : ''}
                    </div>
                </li>`;
            } else if (node.type === 'folder') {
                html += `<li class="tree-node">
                    <div class="tree-item">
                        <span class="tree-toggle" data-target="${nodeId}" style="cursor:pointer;">โ–ผ</span>
                        <i class="fas fa-folder tree-label-icon" style="color:#e6a017;"></i>
                        <span class="tree-label">๐Ÿ“ ${escapeHtml(node.name)}</span>
                    </div>
                    <div id="${nodeId}" class="children-container">
                        ${node.children?.length ? '<ul class="tree-node" style="list-style:none;">' + renderTreeNodes(node.children) + '</ul>' : ''}
                        ${node.items?.length ? renderFileItems(node.items) : ''}
                    </div>
                </li>`;
            }
        }
        return html;
    }

    function renderFileItems(items) {
        let html = '<ul class="tree-node" style="list-style:none;">';
        for (const rec of items) {
            html += `<li class="tree-node">
                <div class="tree-item">
                    <span style="width:20px;display:inline-block;"></span>
                    <i class="fas fa-file tree-label-icon" style="color:#6c8ea0;"></i>
                    <span class="tree-label">
                        <strong>${escapeHtml(rec.File_Name)}</strong> 
                        <div class="tree-details">
                            <span>๐Ÿ“ ${formatNumber(rec.File_Size_GB, 1)} GB</span>
                            <span>๐ŸŽ—๏ธ Segment ${rec.FileSegmentNumber || 0}</span>
                            <span>๐Ÿท๏ธ ${escapeHtml(rec.BarcodeID)}</span>
                            <span>${rec.IsTapeProtected ? '๐Ÿ”’ Protected' : '๐Ÿ”“ Unprotected'}</span>
                            <span>๐Ÿ“€ Pool: ${escapeHtml(rec.Tape_Media_Pool || '-')}</span>
                        </div>
                    </span>
                </div>
            </li>`;
        }
        html += '</ul>';
        return html;
    }

    function updateStats(records) {
        const totalFiltered = records.length;
        let totalSize = 0;
        let protectedCnt = 0;
        const tapeIDs = new Set();
        for (let rec of records) {
            totalSize += (rec.File_Size_GB || 0);
            if (rec.IsTapeProtected === true) protectedCnt++;
            if (rec.BarcodeID) tapeIDs.add(rec.BarcodeID);
        }
        totalRowsSpan.innerText = totalFiltered;
        totalSizeSpan.innerText = formatNumber(totalSize, 1);
        protectedCountSpan.innerText = protectedCnt;
        uniqueTapesSpan.innerText = tapeIDs.size;
    }

    function refreshDisplay() {
        if (!dataset.length) return;
        const filtered = filterData();
        updateStats(filtered);
        if (currentView === 'table') {
            tableViewDiv.style.display = 'block';
            treeViewDiv.style.display = 'none';
            renderTable(filtered);
            toggleViewBtn.innerHTML = '<i class="fas fa-folder-tree"></i> Hierarchy';
        } else {
            tableViewDiv.style.display = 'none';
            treeViewDiv.style.display = 'block';
            renderTree(filtered);
            toggleViewBtn.innerHTML = '<i class="fas fa-table"></i> Table';
        }
    }

    function toggleView() {
        currentView = currentView === 'table' ? 'tree' : 'table';
        refreshDisplay();
    }

    function printCurrentView() {
        if (!dataset.length) {
            alert("No data to print. Please load a CSV file first.");
            return;
        }
        updatePrintHeader();
        if (currentView === 'table') {
            document.body.classList.add('printing-table');
            document.body.classList.remove('printing-tree');
        } else {
            document.body.classList.add('printing-tree');
            document.body.classList.remove('printing-table');
        }
        window.print();
        setTimeout(() => {
            document.body.classList.remove('printing-table', 'printing-tree');
        }, 100);
    }

    function loadFromCsvText(csvText, fileLabel) {
        try {
            const parsed = parseCSVToObjects(csvText);
            if (!parsed.length) {
                loadStatusSpan.innerHTML = '<i class="fas fa-exclamation-triangle"></i> File contains no valid data or headers do not match.';
                return false;
            }
            dataset = parsed;
            currentFileName = fileLabel;
            updateMediaPoolFilter(dataset);
            searchInput.value = '';
            filterProtected.value = 'all';
            filterMediaPool.value = 'all';
            currentView = 'table';
            refreshDisplay();
            loadStatusSpan.innerHTML = `<i class="fas fa-check-circle"></i> Loaded ${dataset.length} records (${fileLabel})`;
            filePathDisplaySpan.innerText = fileLabel;
            controlsPanel.style.display = 'flex';
            document.title = `Veeam Tape Inventory Explorer โ€” ${fileLabel}`;
            updatePrintHeader();
            return true;
        } catch (err) {
            loadStatusSpan.innerHTML = `<i class="fas fa-times-circle"></i> Error: ${err.message}`;
            return false;
        }
    }

    function loadFromFile(file) {
        if (!file) return;
        loadStatusSpan.innerHTML = '<i class="fas fa-spinner fa-pulse"></i> Reading file...';
        const reader = new FileReader();
        reader.onload = function(e) { loadFromCsvText(e.target.result, file.name); };
        reader.onerror = function() { loadStatusSpan.innerHTML = '<i class="fas fa-times-circle"></i> File read error.'; };
        reader.readAsText(file, 'UTF-8');
    }

    function init() {
        chooseFileBtn.addEventListener('click', () => fileInput.click());
        fileInput.addEventListener('change', (e) => {
            if (e.target.files && e.target.files.length > 0) loadFromFile(e.target.files[0]);
            fileInput.value = '';
        });
        resetBtn.addEventListener('click', () => {
            if (dataset.length) {
                searchInput.value = '';
                filterProtected.value = 'all';
                filterMediaPool.value = 'all';
                refreshDisplay();
                loadStatusSpan.innerHTML = `<i class="fas fa-eraser"></i> Filters reset, showing all ${dataset.length} records.`;
            }
        });
        printBtn.addEventListener('click', printCurrentView);
        toggleViewBtn.addEventListener('click', toggleView);
        searchInput.addEventListener('input', refreshDisplay);
        filterProtected.addEventListener('change', refreshDisplay);
        filterMediaPool.addEventListener('change', refreshDisplay);
        controlsPanel.style.display = 'none';
        updatePrintHeader();
    }
    init();
</script>
</body>
</html>
david.domask
Product Manager
Posts: 3623
Liked: 878 times
Joined: Jun 28, 2016 12:12 pm
Contact:

Re: Get tape content via powershell

Post by david.domask »

Hi imaxiss, welcome to the forums.

Thanks for testing and sharing your HTML for formatting the output!
David Domask | Product Management: Principal Analyst
Post Reply

Who is online

Users browsing this forum: No registered users and 9 guests