Skip to main content
The File Browser is the core interface of r2Vault, providing a native macOS experience for browsing, organizing, and managing files in your Cloudflare R2 bucket.

Finder-Style Interface

r2Vault’s browser is designed to feel like a natural extension of macOS Finder, with familiar navigation patterns and visual design.

Path Bar Navigation

Breadcrumb-style path bar shows your current location in the bucket hierarchy

View Modes

Switch between Icons and List views to match your workflow

Multi-Select

Select multiple files and folders for batch operations

Quick Actions

Context menus and keyboard shortcuts for fast file management

View Modes

Switch between two view modes to match your workflow:

Icons View

Grid layout with large file thumbnails, perfect for visual browsing of images and media files.
BrowserView.swift:154-192
private var iconsView: some View {
    ScrollView {
        LazyVGrid(
            columns: [GridItem(.adaptive(minimum: 120, maximum: 140), spacing: 12)],
            spacing: 12
        ) {
            ForEach(viewModel.allBrowserItems) { item in
                FinderIconCell(
                    object: item,
                    credentials: viewModel.credentials ?? .empty,
                    isSelected: viewModel.selectedObjectIDs.contains(item.id)
                )
                .onTapGesture(count: 2) {
                    if item.isFolder {
                        viewModel.navigateToFolder(item)
                    } else {
                        QuickLookCoordinator.shared.preview(
                            item,
                            credentials: viewModel.credentials ?? .empty
                        )
                    }
                }
                .onTapGesture(count: 1) {
                    toggleSelection(item)
                }
                .contextMenu { rowContextMenu(for: item) }
            }
        }
        .padding(16)
    }
}
The icons view uses a lazy grid with adaptive columns that automatically adjust based on window size.

List View

Compact list layout with sortable columns (Name, Kind, Size, Date Modified), ideal for browsing large folders.
BrowserView.swift:196-218
private var listView: some View {
    VStack(spacing: 0) {
        listHeader
        Divider()
        List {
            ForEach(viewModel.allBrowserItems) { item in
                R2ObjectRow(
                    object: item,
                    credentials: viewModel.credentials ?? .empty,
                    isSelected: viewModel.selectedObjectIDs.contains(item.id),
                    onNavigate: { viewModel.navigateToFolder($0) },
                    onCopyURL: { viewModel.copyToClipboard($0) },
                    onPreview: { QuickLookCoordinator.shared.preview($0, credentials: viewModel.credentials ?? .empty) },
                    onDownload: { viewModel.downloadToDestination(from: $0, dest: $1) },
                    onDelete: { item in Task { await viewModel.deleteObject(item) } }
                )
                .onTapGesture { toggleSelection(item) }
                .contextMenu { rowContextMenu(for: item) }
            }
        }
        .listStyle(.inset)
    }
}
The path bar at the top of the browser shows your current location and allows instant navigation to any parent folder.
BrowserView.swift:82-112
private var pathBar: some View {
    ScrollView(.horizontal, showsIndicators: false) {
        HStack(spacing: 2) {
            pathCrumb(
                label: viewModel.credentials?.bucketName ?? "Bucket",
                systemImage: "externaldrive.fill",
                isCurrent: viewModel.pathSegments.isEmpty
            ) { viewModel.navigateToRoot() }

            ForEach(Array(viewModel.pathSegments.enumerated()), id: \.offset) { idx, segment in
                Image(systemName: "chevron.right")
                    .font(.system(size: 9, weight: .semibold))
                    .foregroundStyle(.tertiary)
                pathCrumb(
                    label: segment,
                    systemImage: "folder.fill",
                    isCurrent: idx == viewModel.pathSegments.count - 1
                ) { viewModel.navigateToSegment(idx) }
            }
        }
        .padding(.horizontal, 14)
        .animation(.spring(response: 0.3, dampingFraction: 0.85), value: viewModel.pathSegments)
    }
    .frame(height: 30)
    .background(.regularMaterial)
}
Click any folder name in the path bar to instantly jump to that location in your bucket.

Search and Filtering

The search bar in the toolbar provides real-time filtering of files and folders by name.
BrowserView.swift:40
.searchable(text: $vm.filterText, placement: .toolbar, prompt: "Search")
Filtering logic is implemented in the view model:
AppViewModel.swift:100-131
var allBrowserItems: [R2Object] {
    let combined = browserFolders + browserObjects
    let filtered: [R2Object]
    if filterText.isEmpty {
        filtered = combined
    } else {
        filtered = combined.filter { $0.name.localizedCaseInsensitiveContains(filterText) }
    }
    return filtered.sorted { a, b in
        // Folders always sort before files when sorting by name/date/kind
        if sortKey != .size && a.isFolder != b.isFolder {
            return a.isFolder
        }
        let result: Bool
        switch sortKey {
        case .name:
            result = a.name.localizedCompare(b.name) == .orderedAscending
        case .size:
            result = a.size < b.size
        case .date:
            let aDate = a.lastModified ?? .distantPast
            let bDate = b.lastModified ?? .distantPast
            result = aDate < bDate
        case .kind:
            result = a.isFolder == b.isFolder
                ? a.name.localizedCompare(b.name) == .orderedAscending
                : a.isFolder
        }
        return sortAscending ? result : !result
    }
}

Quick Look Integration

Double-click any file to instantly preview it using macOS Quick Look. No download required - files stream directly from R2 via presigned URLs.
QuickLookCoordinator.swift:27-45
func preview(_ object: R2Object, credentials: R2Credentials) {
    let panel = QLPreviewPanel.shared()!

    // Toggle: if already visible for same coordinator, close it
    if panel.isVisible && panel.dataSource === self {
        panel.close()
        return
    }

    // Generate a presigned URL and pass it directly to Quick Look — no download needed.
    // previewItemTitle supplies the filename so QL picks the right previewer plugin.
    guard let presignedURL = AWSV4Signer.presignedURL(for: object.key, credentials: credentials) else { return }
    previewItem = PreviewItem(url: presignedURL, title: object.name)

    panel.dataSource = self
    panel.delegate  = self
    panel.makeKeyAndOrderFront(nil)
    panel.reloadData()
}
Quick Look supports images, PDFs, videos, audio, documents, and most common file formats - all streaming directly from R2 without downloading to disk.

Drag and Drop Upload

Drag files or folders from Finder directly into the browser window to upload them to the current folder.
BrowserView.swift:27-32
mainContent
    .dropDestination(for: URL.self) { urls, _ in
        guard viewModel.hasCredentials else { return false }
        viewModel.handleDroppedURLs(urls)
        return true
    } isTargeted: { isDropTargeted = $0 }
When files are dropped, the view model handles both individual files and folders recursively:
AppViewModel.swift:359-420
func handleDroppedURLs(_ urls: [URL]) {
    var tasks: [FileUploadTask] = []

    for url in urls {
        let accessing = url.startAccessingSecurityScopedResource()
        defer { if accessing { url.stopAccessingSecurityScopedResource() } }

        var isDirectory: ObjCBool = false
        let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
        guard exists else { continue }

        if isDirectory.boolValue {
            // Recursively enumerate directory contents
            let dirName = url.lastPathComponent
            let folderBookmark = try? url.bookmarkData(options: [.withSecurityScope],
                                                     includingResourceValuesForKeys: nil,
                                                     relativeTo: nil)
            guard let enumerator = FileManager.default.enumerator(
                at: url,
                includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey],
                options: [.skipsHiddenFiles]
            ) else { continue }

            for case let fileURL as URL in enumerator {
                // Process each file in the folder...
                let relativePath = fileURL.path.replacingOccurrences(
                    of: url.deletingLastPathComponent().path + "/",
                    with: ""
                )
                let r2Key = currentPrefix + relativePath
                let task = FileUploadTask(fileURL: fileURL, fileName: fileName, fileSize: fileSize)
                task.uploadKey = r2Key
                task.parentFolderBookmark = folderBookmark
                tasks.append(task)
            }
        } else {
            // Single file — upload into current prefix
            let task = FileUploadTask(fileURL: url, fileName: fileName, fileSize: fileSize)
            task.uploadKey = currentPrefix + fileName
            tasks.append(task)
        }
    }

    guard !tasks.isEmpty else { return }
    uploadTasks.append(contentsOf: tasks)
    Task { await uploadPendingTasks() }
}
When you drag a folder, r2Vault preserves the complete folder structure in R2, including all subfolders and their contents.

Keyboard Shortcuts

  • ⌘N - New folder
  • ⌘O - Upload files
  • Delete - Delete selected items
  • ⌘R - Refresh current folder
  • ⌘A - Select all
  • ⌘D - Deselect all
  • Click - Single selection
  • ⌘Click - Add to selection

Context Menus

Right-click any file or folder to access quick actions:
BrowserView.swift:353-381
@ViewBuilder
private func rowContextMenu(for item: R2Object) -> some View {
    if item.isFolder {
        Button { viewModel.navigateToFolder(item) } label: {
            Label("Open", systemImage: "arrow.right.circle")
        }
        Divider()
    } else {
        Button {
            QuickLookCoordinator.shared.preview(item, credentials: viewModel.credentials ?? .empty)
        } label: {
            Label("Preview", systemImage: "eye")
        }
        Button {
            let url = (viewModel.credentials ?? .empty).publicURL(forKey: item.key)
            viewModel.copyToClipboard(url.absoluteString)
        } label: {
            Label("Copy URL", systemImage: "doc.on.clipboard")
        }
        Divider()
    }
    Button { viewModel.selectedObjectIDs.insert(item.id) } label: {
        Label("Select", systemImage: "checkmark.circle")
    }
    Divider()
    Button(role: .destructive) { pendingDeleteItem = item } label: {
        Label(item.isFolder ? "Delete Folder & Contents" : "Delete", systemImage: "trash")
    }
}
Deleting a folder will recursively delete all files and subfolders inside it. This action cannot be undone.

Toolbar

The browser toolbar provides quick access to common actions:
1

Back/Forward Buttons

Navigate through your browsing history
2

View Mode Toggle

Switch between Icons and List view
3

Upload Menu

Upload files, upload folder, or create new folder
4

Refresh Button

Reload the current folder contents
5

Sort Menu

Sort by Name, Size, Date, or Kind
6

Selection Menu

Select all, deselect, or delete selected items

Status Bar

The status bar at the bottom shows the current selection state:
BrowserView.swift:322-349
private var statusBar: some View {
    let total = viewModel.allBrowserItems.count
    let sel   = viewModel.selectedObjectIDs.count
    return Group {
        if total > 0 || sel > 0 {
            HStack(spacing: 10) {
                if sel > 0 {
                    Text("\(sel) of \(total) selected").foregroundStyle(.secondary)
                    Spacer()
                    Button(role: .destructive) { showDeleteConfirm = true } label: {
                        Label("Delete Selected", systemImage: "trash")
                    }
                    .buttonStyle(.plain).foregroundStyle(.red)
                    Button { viewModel.clearSelection() } label: { Text("Deselect") }
                        .buttonStyle(.plain).foregroundStyle(Color.accentColor)
                } else {
                    Text("\(total) item\(total == 1 ? "" : "s")").foregroundStyle(.secondary)
                    Spacer()
                }
            }
            .font(.caption)
            .padding(.horizontal, 12).padding(.vertical, 5)
            .background(.bar)
            .overlay(alignment: .top) { Divider() }
        }
    }
}
The status bar only appears when there are items in the current folder or when items are selected.