From f2160a776861881210154dcb624bc687776dc59c Mon Sep 17 00:00:00 2001 From: akhmed Date: Tue, 15 Sep 2026 22:22:52 +0300 Subject: [PATCH] =?UTF-8?q?v1.0.3:=20=D0=94=D0=BE=D1=81=D1=82=D1=83=D0=BF?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=BC=D0=BD=D0=B5=20=E2=80=94=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B5=20=D0=BE=D0=B1=D0=BD?= =?UTF-8?q?=D0=B0=D1=80=D1=83=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=89=D0=B8=D1=85=20=D0=BF=D0=B0=D0=BF=D0=BE=D0=BA=20(=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=B5=20share/is=5Fowned)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Models: ShareInfo (rights, is_owned, is_root) + поле share в DiskResource - DiskAPIClient.sharedWithMe(): обход дерева, сбор ресурсов с is_owned == false - Вкладка «Доступные мне» показывает общие папки, в контекстном меню права rw/r - Реестр публичных ссылок из v1.0.2 убран (был workaround) --- README.md | 2 +- Sources/YanDisk/Core/AppStore.swift | 64 +++--------------------- Sources/YanDisk/Core/DiskAPIClient.swift | 22 ++++++++ Sources/YanDisk/Core/Models.swift | 19 ++++++- Sources/YanDisk/UI/MainSplitView.swift | 38 ++++---------- ПРАВКИ.md | 4 +- 6 files changed, 61 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index d9647ce..9eeb2c3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ - Авторизация OAuth (токен в Keychain) - Просмотр Диска: все файлы и папки, «Недавние», «Корзина» - **Избранное**: правый клик → «В избранное» (метка хранится локально, звезда в списке) -- **Доступные мне**: реестр публичных ссылок, полученных от других пользователей (кнопка «+» на вкладке; открытие в браузере, копирование, удаление) +- **Доступные мне**: папки, к которым доступ дали другие пользователи (обнаружение по полю `share.is_owned == false` в листинге API; права rw/r в контекстном меню) - Чтение / скачивание, загрузка (кнопка и drag-and-drop) - Копирование, перемещение, переименование, дублирование - Удаление в Корзину, восстановление, очистка Корзины diff --git a/Sources/YanDisk/Core/AppStore.swift b/Sources/YanDisk/Core/AppStore.swift index 216f636..155d6d8 100644 --- a/Sources/YanDisk/Core/AppStore.swift +++ b/Sources/YanDisk/Core/AppStore.swift @@ -75,7 +75,6 @@ final class AppStore { private var api: DiskAPIClient? private static let favoritesKey = "FavoritePaths" - private static let publicLinksKey = "PublicLinks" var usedPercent: Double { guard let info = diskInfo, let total = info.totalSpace, total > 0 else { return 0 } @@ -250,57 +249,20 @@ final class AppStore { } } - // MARK: - Shared with me (public links) - - func addPublicLink(_ rawLink: String) async { - let link = rawLink.trimmingCharacters(in: .whitespacesAndNewlines) - let links = UserDefaults.standard.stringArray(forKey: Self.publicLinksKey) ?? [] - let isDiskLink = link.contains("disk.yandex.") || link.contains("yadi.sk") - guard isDiskLink, !publicItems.contains(where: { $0.path == link }), - !links.contains(link) else { - errorMessage = "Некорректная публичная ссылка" - return - } - guard let api else { return } - isLoading = true - defer { isLoading = false } - do { - let resource = try await api.publicResource(publicKey: link) - publicItems.append(.presentedByLink(resource, link: link)) - var links = UserDefaults.standard.stringArray(forKey: Self.publicLinksKey) ?? [] - links.append(link) - UserDefaults.standard.set(links, forKey: Self.publicLinksKey) - statusMessage = "Добавлено: \(resource.displayName)" - errorMessage = nil - } catch { - handleError(error) - } - } - - func removePublicLink(_ item: DiskResource) { - publicItems.removeAll { $0.path == item.path } - var links = UserDefaults.standard.stringArray(forKey: Self.publicLinksKey) ?? [] - links.removeAll { $0 == item.path } - UserDefaults.standard.set(links, forKey: Self.publicLinksKey) - statusMessage = "Убрано: \(item.displayName)" - } + // MARK: - Shared with me (field `share`, is_owned == false) func loadPublicItems() async { guard let api else { return } isLoading = true defer { isLoading = false } - let links = UserDefaults.standard.stringArray(forKey: Self.publicLinksKey) ?? [] - if links.count != publicItems.count { - var loaded: [DiskResource] = [] - for link in links { - do { - let r = try await api.publicResource(publicKey: link) - loaded.append(.presentedByLink(r, link: link)) - } catch { - handleError(error) - } + do { + let shared = try await api.sharedWithMe() + publicItems = shared.sorted { a, b in + a.displayName.localizedCaseInsensitiveCompare(b.displayName) == .orderedAscending } - publicItems = loaded + errorMessage = nil + } catch { + handleError(error) } } @@ -583,16 +545,6 @@ final class AppStore { } } -extension DiskResource { - /// Ресурс, представленный публичной ссылкой: path = сама ссылка. - static func presentedByLink(_ resource: DiskResource, link: String) -> DiskResource { - DiskResource( - name: resource.name, created: resource.created, modified: resource.modified, - path: link, type: resource.type, md5: nil, size: resource.size, mimeType: resource.mimeType, - preview: resource.preview, publicKey: link, publicUrl: link, originPath: nil, embedded: nil) - } -} - enum ByteFormatter { static func string(info: Int64) -> String { let formatter = Foundation.ByteCountFormatter() diff --git a/Sources/YanDisk/Core/DiskAPIClient.swift b/Sources/YanDisk/Core/DiskAPIClient.swift index b138e07..90876c0 100644 --- a/Sources/YanDisk/Core/DiskAPIClient.swift +++ b/Sources/YanDisk/Core/DiskAPIClient.swift @@ -371,4 +371,26 @@ actor DiskAPIClient { let (data, _) = try await send(request) return try decode(DiskResource.self, from: data) } + + /// Ресурсы, к которым доступ дали другие пользователи. + /// Ищем по дереву поле `share` с `is_owned == false` (недокументированное, но в каждом ответе API). + func sharedWithMe(from root: String = "/", maxDepth: Int = 3) async throws -> [DiskResource] { + var result: [DiskResource] = [] + + func walk(_ path: String, depth: Int) async throws { + guard depth <= maxDepth else { return } + for item in try await listDirectory(path: path) { + if item.share?.isSharedWithMe == true { + result.append(item) + continue // внутрь чужих общих папок не заходим — это отдельные корни + } + if item.isDirectory { + try await walk(item.path, depth: depth + 1) + } + } + } + + try await walk(root, depth: 0) + return result + } } diff --git a/Sources/YanDisk/Core/Models.swift b/Sources/YanDisk/Core/Models.swift index 76c15ff..4ba3287 100644 --- a/Sources/YanDisk/Core/Models.swift +++ b/Sources/YanDisk/Core/Models.swift @@ -5,6 +5,22 @@ enum ResourceType: String, Codable, Sendable { case dir } +/// Информация об общем доступе к ресурсу (недокументированное поле `share`). +struct ShareInfo: Codable, Hashable, Sendable { + let rights: String? + let isOwned: Bool? + let isRoot: Bool? + + enum CodingKeys: String, CodingKey { + case rights + case isOwned = "is_owned" + case isRoot = "is_root" + } + + /// Ресурс, к которому доступ дали другие пользователи. + var isSharedWithMe: Bool { isOwned == false } +} + struct DiskResource: Codable, Identifiable, Hashable, Sendable { let name: String? let created: String? @@ -18,10 +34,11 @@ struct DiskResource: Codable, Identifiable, Hashable, Sendable { let publicKey: String? let publicUrl: String? let originPath: String? + let share: ShareInfo? let embedded: ResourceList? enum CodingKeys: String, CodingKey { - case name, created, modified, path, type, md5, size, preview + case name, created, modified, path, type, md5, size, preview, share case mimeType = "mime_type" case publicKey = "public_key" case publicUrl = "public_url" diff --git a/Sources/YanDisk/UI/MainSplitView.swift b/Sources/YanDisk/UI/MainSplitView.swift index 094aa5a..a4c940f 100644 --- a/Sources/YanDisk/UI/MainSplitView.swift +++ b/Sources/YanDisk/UI/MainSplitView.swift @@ -6,8 +6,6 @@ struct MainSplitView: View { @Environment(\.openWindow) private var openWindow @State private var showNewFolderAlert = false @State private var newFolderName = "" - @State private var showAddLinkAlert = false - @State private var newLinkText = "" @State private var renameItem: DiskResource? @State private var renameText = "" @@ -58,14 +56,6 @@ struct MainSplitView: View { } .toolbar { ToolbarItemGroup { - if store.sidebarSelection == .files { - Button { - showAddLinkAlert = true - } label: { - Label("Добавить публичную ссылку", systemImage: "plus") - } - } - Button { showNewFolderAlert = true } label: { @@ -126,17 +116,6 @@ struct MainSplitView: View { } Button("Отмена", role: .cancel) {} } - .alert("Публичная ссылка", isPresented: $showAddLinkAlert) { - TextField("https://disk.yandex.ru/d/…", text: $newLinkText) - Button("Добавить") { - let link = newLinkText - newLinkText = "" - Task { await store.addPublicLink(link) } - } - Button("Отмена", role: .cancel) {} - } message: { - Text("Вставьте ссылку, которой поделился другой пользователь (папка «Общий доступ» на disk.yandex.ru → «Поделиться ссылкой»).") - } .alert("Переименовать", isPresented: Binding( get: { renameItem != nil }, set: { if !$0 { renameItem = nil } } @@ -222,7 +201,7 @@ struct MainSplitView: View { switch store.sidebarSelection { case .trash: return "Корзина пуста" case .favorites: return "Нет избранного — правый клик по файлу → «В избранное»" - case .files: return "Нет общих ссылок — нажмите «+», чтобы добавить публичную ссылку" + case .files: return "Нет папок с общим доступом" default: return "Папка пуста" } } @@ -308,15 +287,18 @@ struct MainSplitView: View { Task { await store.restoreFromTrash(item) } } } else if store.sidebarSelection == .files { - Button("Открыть в браузере") { store.open(item) } - Button("Скопировать ссылку") { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(item.path, forType: .string) + Button("Открыть") { store.open(item) } + if let url = item.publicUrl, !url.isEmpty { + Button("Скопировать публичную ссылку") { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(url, forType: .string) + } } Divider() - Button("Убрать из «Доступные мне»", role: .destructive) { - store.removePublicLink(item) + Menu("Права доступа") { + Text(item.share?.rights == "rw" ? "Редактирование (rw)" : "Только просмотр (r)") } + .disabled(true) } else { Button(store.isFavorite(item) ? "Убрать из избранного" : "В избранное") { store.toggleFavorite(item) diff --git a/ПРАВКИ.md b/ПРАВКИ.md index 3f01a98..0ec94c9 100644 --- a/ПРАВКИ.md +++ b/ПРАВКИ.md @@ -12,9 +12,9 @@ | 4 | Режим синхронизации по умолчанию — «Только в облако» (`SyncPolicyStore.globalMode = .cloudOnly`) | `SyncEngine.swift` | ### По пункту 3 — «Доступные мне» -В открытом REST API Яндекс.Диска (`cloud-api.yandex.net`) нет эндпоинта «файлы, которыми поделились со мной». Проверено эмпирически (15.09.2026): web-API `disk.yandex.ru/api/v2` за сессией+капчей (OAuth-токен не принимается, 302 → captcha/passport), в WebDAV и в дереве Диска общие папки не отображаются, в `system_folders` соответствующего пути нет. +В документации REST API нет эндпоинта «файлы, которыми поделились со мной», и web-API `disk.yandex.ru/api/v2` требует браузерную сессию (302 → капча). -**Реализовано (v1.0.2):** вкладка «Доступные мне» = реестр публичных ссылок, добавленных пользователем (кнопка «+»); метаданные тянутся официальным `GET /v1/disk/public/resources?public_key=…`, открытие в браузере, копирование и удаление ссылки из реестра. Хранилище — UserDefaults (`PublicLinks`). +**Найдено и реализовано (v1.0.3):** в ответе `GET /v1/disk/resources` (листинг любого каталога) каждый ресурс несёт недокументированное поле `share: {rights, is_owned, is_root}`. Папки, к которым доступ дали другие пользователи, лежат прямо в дереве Диска с `is_owned: false`. `DiskAPIClient.sharedWithMe()` обходит дерево (до глубины 3, внутрь чужих общих корней не заходит) и собирает все ресурсы с `is_owned == false`. Вкладка «Доступные мне» показывает их с правами (`rw`/`r`) в контекстном меню. ---