v1.0.3: Доступные мне — реальное обнаружение общих папок (поле share/is_owned)
- Models: ShareInfo (rights, is_owned, is_root) + поле share в DiskResource - DiskAPIClient.sharedWithMe(): обход дерева, сбор ресурсов с is_owned == false - Вкладка «Доступные мне» показывает общие папки, в контекстном меню права rw/r - Реестр публичных ссылок из v1.0.2 убран (был workaround)
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
- Авторизация OAuth (токен в Keychain)
|
||||
- Просмотр Диска: все файлы и папки, «Недавние», «Корзина»
|
||||
- **Избранное**: правый клик → «В избранное» (метка хранится локально, звезда в списке)
|
||||
- **Доступные мне**: реестр публичных ссылок, полученных от других пользователей (кнопка «+» на вкладке; открытие в браузере, копирование, удаление)
|
||||
- **Доступные мне**: папки, к которым доступ дали другие пользователи (обнаружение по полю `share.is_owned == false` в листинге API; права rw/r в контекстном меню)
|
||||
- Чтение / скачивание, загрузка (кнопка и drag-and-drop)
|
||||
- Копирование, перемещение, переименование, дублирование
|
||||
- Удаление в Корзину, восстановление, очистка Корзины
|
||||
|
||||
@@ -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,59 +249,22 @@ 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))
|
||||
let shared = try await api.sharedWithMe()
|
||||
publicItems = shared.sorted { a, b in
|
||||
a.displayName.localizedCaseInsensitiveCompare(b.displayName) == .orderedAscending
|
||||
}
|
||||
errorMessage = nil
|
||||
} catch {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
publicItems = loaded
|
||||
}
|
||||
}
|
||||
|
||||
func loadDiskInfo() async {
|
||||
guard let api else { return }
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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("Скопировать ссылку") {
|
||||
Button("Открыть") { store.open(item) }
|
||||
if let url = item.publicUrl, !url.isEmpty {
|
||||
Button("Скопировать публичную ссылку") {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(item.path, forType: .string)
|
||||
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)
|
||||
|
||||
@@ -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`) в контекстном меню.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user