#
简介智汀云盘 是一款在 Swift 5 中开发的 iOS 云盘APP。该应用一直在积极升级以采用 iOS 和 Swift 语言的最新功能。此应用配合智汀家庭云这款智能家居APP授权使用。
#
1. 快速上手#
1.1 开发工具- 当前版本适用于 Xcode 版本 Xcode 13 。如果您使用不同的 Xcode 版本,请查看之前的版本。
- 此版本为仅使用 Swift 5 支持 iOS 13+。
#
1.2 源码地址Git Hub
名称 URL 描述 zhiting-nas-ios https://github.com/zhiting-tech/sa-ios-sdk iOS源码 gitee
名称 URL 描述 zhiting-nas-ios https://gitee.com/zhiting-tech/sa-ios-sdk iOS源码
#
1.3 构建版本克隆存储库
bash $ git clone https://xxxx/sa-ios-sdk.git
CocoaPods
安装CocoaPods,详情可查询CocoaPods
CocoaPods安装完成后,请用CocoaPods下载第三方库
$ cd sa-ios-sdk$ pod install
如遇下载失败,则可能需科学上网,自行配置网络代理。
运行程序
pod install 成功后,在模拟器中编译并运行应用程序。
如果您没有看到任何数据,请检查 "Simulator" -> "Debug" -> "Location" 以更改位置。
#
2. 开发指南#
2.1 设计模式本项目采用MVC设计模式
#
2.2 组织架构项目组织架构如下图
#
2.3 授权登录登录授权页面点击按钮后通过URLSchema的方式唤醒智汀家庭云App进行授权
loginBtn.clickCallBack = { [weak self] _ in guard let self = self else { return } if let url = URL(string: "zhiting://operatioaction=diskAuth"), UIApplication.shared.canOpenU(url) { print("跳转成功") UIApplication.shared.open(url, options: [:]completionHandler: nil) } else { self.showToast("请先安装 \"智汀家庭云\" APP") print("跳转失败") }}
智汀家庭云授权部分代码片段:
/// 授权当前绑定SA的家庭 private func requestAreaScopeToken() { let scopes = authItems.filter({ $0.isSelected }).map(\.name) if !authManager.currentArea.is_bind_sa { showToast(string: "当前家庭未绑定SA".localizedString) confirmButton.selectedChangeView(isLoading: false) return }
ApiServiceManager.shared.scopeToken(area: authManager.currentArea, scopes: scopes) { [weak self] response in guard let self = self else { return } let area = self.transferToAuthedArea(from: self.authManager.currentArea, scopeTokenModel: response.scope_token) if self.authManager.isLogin { self.returnAuthResult(cloud_user_id: self.authManager.currentArea.cloud_user_id, cloud_url: cloudUrl, areas: [area]) } else { area.id = 0 self.returnAuthResult(cloud_user_id: nil, cloud_url: nil, areas: [area]) } } failureCallback: { [weak self] code, err in self?.showToast(string: err) self?.confirmButton.selectedChangeView(isLoading: false) } } private func returnAuthResult(cloud_user_id: Int?, cloud_url: String?, areas: [AuthedAreaModel]) { let result = ResultModel() result.cloud_url = cloud_url result.cloud_user_id = cloud_user_id result.nickname = authManager.currentUser.nickname result.areas = areas if let cloudUrl = cloud_url, let cookie = HTTPCookieStorage.shared.cookies?.first(where: { cloudUrl.contains($0.domain) }) { result.sessionCookie = cookie.value } guard let json = result.toJSONString(), let data = try? NSKeyedArchiver.archivedData(withRootObject: json, requiringSecureCoding: true) else { confirmButton.selectedChangeView(isLoading: false) return } try? data.write(to: shareTokenURL, options: .atomic) confirmButton.selectedChangeView(isLoading: false) if let url = URL(string: "zhitingcloud://operation?action=auth") { UIApplication.shared.open(url, options: [:], completionHandler: nil) } dismiss(animated: true, completion: nil) }
智汀App授权成功后将结果归档并写入到AppGroup共享空间的shareToken.plist文件中,然后通过URLSchema的方式唤醒智汀网盘App,智汀网盘App解析URLSchema,判断是授权成功响应时获取AppGroup共享空间的shareToken.plist文件内容并进行解档得到授权的家庭、用户和云端账号cookies等信息,储存信息并跳转页面.(详情见OpenUrlManager类)
OpenUrlManager类:
import Foundation
// MARK: - OpenUrlManagerclass OpenUrlManager { enum Action: String { /// 网盘授权 case auth }
static var shared = OpenUrlManager()
private init() {}
func open(url: URL) { let urlString = url.absoluteString print("--------------------- open from other app ----------------------------------") print(Date()) print("---------------------------------------------------------------------------") print("open url from \(urlString)") print("---------------------------------------------------------------------------\n\n") guard let components = urlString.components(separatedBy: "zhitingcloud://operation?").last, let action = components.components(separatedBy: "&").first?.components(separatedBy: "=").last else { return } switch Action(rawValue: action) { case .auth: // zhitingcloud://operation?action=auth let shareTokenURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.zhiting.tech")!.appendingPathComponent("shareToken.plist") /// 读取授权成功响应并解密 guard let data = try? Data(contentsOf: shareTokenURL), let json = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? String else { return } if let authResult = AuthModel.deserialize(from: json) { let user = User() user.nickname = authResult.nickname
/// 存储家庭信息 AreaManager.shared.cacheAreas(areas: authResult.areas) if let area = AreaManager.shared.getAreaList().first { AreaManager.shared.currentArea = area user.user_id = area.sa_user_id } if let cloudUserId = authResult.cloud_user_id, let cloudUrl = authResult.cloud_url, let cookieValue = authResult.sessionCookie { /// 云端的授权 user.user_id = cloudUserId UserManager.shared.isCloudUser = true UserManager.shared.cloudUrl = cloudUrl
/// 写入云端账号cookie if let cookie = HTTPCookie(properties: [ HTTPCookiePropertyKey.domain : cloudUrl, HTTPCookiePropertyKey.value : cookieValue, HTTPCookiePropertyKey.path : "/", HTTPCookiePropertyKey.name : "_session_" ]) { HTTPCookieStorage.shared.setCookie(cookie) } } UserManager.shared.currentUser = user UserManager.shared.cacheUser(user: user) } SceneDelegate.shared.window?.rootViewController = TabbarController() default: break } }}
#
2.4 文件列表及权限说明#
2.4.1 文件列表#
2.4.1.1 我的文件——MyFileViewController.swift- 数据获取——通过SA接口获取数据
NetworkManager.shared.fileList(path: "/", page: page, page_size: 30) { [weak self] response in guard let self = self else { return } LoadingView.hide() self.tableView.mj_header?.endRefreshing() self.tableView.mj_footer?.endRefreshing() //删选没有可读权限的文件 let datas = response.list.filter({$0.read != 0}) if isReload {//下拉刷新 or 首次加载数据 if datas.count == 0 { self.tableView.mj_footer?.isHidden = true self.tableView.reloadData() }else{ self.emptyView.removeFromSuperview() self.currentDatas = datas self.tableView.reloadData() } }else{//上拉加载更多数据 if !response.pager.has_more {//已无数据 self.tableView.mj_footer?.endRefreshingWithNoMoreData() self.isGetAllData = true return } self.isGetAllData = false self.currentDatas += datas self.tableView.reloadData() } } failureCallback: {[weak self] code, err in guard let self = self else { return } LoadingView.hide() self.tableView.mj_header?.endRefreshing() self.tableView.mj_footer?.endRefreshing() if self.currentDatas.count == 0 { self.tableView.addSubview(self.emptyView) self.emptyView.snp.makeConstraints { $0.center.equalToSuperview() $0.width.equalTo(Screen.screenWidth) $0.height.equalTo(ZTScaleValue(110)) } }else{ self.emptyView.removeFromSuperview() } self.showToast("\(err)") }
- 此页面无选择功能
- 点击文件夹进入自己目录
let vc = ChangeFolderPlaceController() let nav = UINavigationController(rootViewController: vc) nav.modalPresentationStyle = .fullScreen self.present(nav, animated: true, completion: nil)
#
2.4.1.2 共享文件——ShareFileViewController.swift用户可以获取到其他用户共享出来的文件夹.
/// 请求列表数据private func getDiskData(){ let page = (currentDatas.count / 30) + 1 NetworkManager.shared.shareFileList(page: page, page_size: 30) { [weak self] response in guard let self = self else {return} print("请求成功") LoadingView.hide() self.tableView.mj_header?.endRefreshing() self.tableView.mj_footer?.endRefreshing() //删选没有可读权限的文件 let datas = response.list.filter({$0.read != 0}) self.currentDatas.append(contentsOf: datas) if !response.pager.has_more { self.tableView.mj_footer?.endRefreshingWithNoMoreData() }
if self.currentDatas.count == 0 { //空数据展示页面 self.tableView.addSubview(self.emptyView) self.emptyView.snp.makeConstraints { $0.center.equalToSuperview() $0.width.equalTo(Screen.screenWidth) $0.height.equalTo(ZTScaleValue(110)) } self.tableView.reloadData() }else{ self.emptyView.removeFromSuperview() self.tableView.reloadData() }
} failureCallback: {[weak self] code, err in guard let self = self else { return } print("请求失败") LoadingView.hide() self.tableView.mj_header?.endRefreshing()
if self.currentDatas.count == 0 { //空数据展示页面 self.tableView.addSubview(self.emptyView) self.emptyView.snp.makeConstraints { $0.center.equalToSuperview() $0.width.equalTo(Screen.screenWidth) $0.height.equalTo(ZTScaleValue(110)) } }else{ self.emptyView.removeFromSuperview() } self.showToast("\(err)") } }
- 共享文件与我的文件页面逻辑大致相同,但其他用户共享过来的文件夹允许选择功能模块
// MARK: - funtionTabbarAction funtionTabbarView.shareBtn.clickCallBack = { _ in print("点击分享到") let shareVC = FileShareController() shareVC.fileDatas = self.seletedFiles self.navigationController?.pushViewController(shareVC, animated: true) self.hideFunctionTabbarView() } funtionTabbarView.downloadBtn.clickCallBack = { _ in print("点击下载") } funtionTabbarView.copyBtn.clickCallBack = {[weak self] _ in guard let self = self else {return} print("点击复制到") let vc = ChangeFolderPlaceController() vc.isRootPath = true vc.currentPaths = ["根目录"] vc.seletedFiles = self.seletedFiles vc.type = .copy let nav = UINavigationController(rootViewController: vc) nav.modalPresentationStyle = .fullScreen self.present(nav, animated: true, completion: nil) self.hideAllFuntionView() } funtionTabbarView.resetNameBtn.clickCallBack = { [weak self] _ in guard let self = self else { return } print("点击重命名") guard let file = self.seletedFiles.first else { return } self.showResetNameView(name: file.name, isFile: file.type == 1) self.setNameView?.setNameCallback = { name in if name.isEmpty { SceneDelegate.shared.window?.makeToast("请输入名称".localizedString) return } if let originalExtension = file.name.components(separatedBy: ".").last, let newExtension = name.components(separatedBy: ".").last { if originalExtension != newExtension && file.type == 1 { let alertViewController = UIAlertController(title: "", message: "更改文件类型可能导致文件不可用,是否继续?", preferredStyle: .alert) alertViewController.addAction(UIAlertAction(title: "取消".localizedString, style: .cancel, handler: nil)) alertViewController.addAction(UIAlertAction(title: "确定".localizedString, style: .default, handler: { [weak self] _ in guard let self = self else { return } LoadingView.show() NetworkManager.shared.renameFile(path: file.path, name: name) { [weak self] response in guard let self = self else { return } file.name = name self.setNameView?.removeFromSuperview() self.hideAllFuntionView() self.tableView.reloadData() LoadingView.hide() SceneDelegate.shared.window?.makeToast("重命名成功".localizedString) } failureCallback: { code, err in SceneDelegate.shared.window?.makeToast(err) LoadingView.hide() } })) self.present(alertViewController, animated: true, completion: nil) return } }
#
2.4.1.3 文件夹子集目录——FolderViewController.swift- 子集目录可存在各类型文件的存在形式,根据数据model的type来区分。
- 子集目录点击文件类型是,展示functionView,功能根据权限开放。
- 子集目录文件及文件夹均可选择,展示功能tabbar。
#
2.4.2 文件夹共享用户可以将文件夹共享给选中的用户,并根据自身需求赋予他们读写删的权限.
/// 共享文件夹给用户 private func shareFilesToCloud() { var filePaths = [String]() for file in fileDatas { filePaths.append(file.path) } if seletedUsers.count == 0 { return } let userIds = seletedUsers.map(\.user_id) let editMemberAlert = EditShareMemberAlert() editMemberAlert.set(members: seletedUsers) editMemberAlert.reSetBtn() //权限判断 if self.fileDatas.filter({ $0.read == 0 }).count > 0 {//没有可读权限 editMemberAlert.readBtn.isUserInteractionEnabled = false editMemberAlert.readBtn.alpha = 0.5 } else { editMemberAlert.readBtn.isUserInteractionEnabled = true editMemberAlert.readBtn.alpha = 1 editMemberAlert.readBtn.isSelected = true } if self.fileDatas.filter({ $0.write == 0 }).count > 0 {//没有写入权限 editMemberAlert.writeBtn.isUserInteractionEnabled = false editMemberAlert.writeBtn.alpha = 0.5 } else { editMemberAlert.writeBtn.isUserInteractionEnabled = true editMemberAlert.writeBtn.alpha = 1 } if self.fileDatas.filter({ $0.deleted == 0 }).count > 0 {//没有删权限 editMemberAlert.deleteBtn.isUserInteractionEnabled = false editMemberAlert.deleteBtn.alpha = 0.5 } else { editMemberAlert.deleteBtn.isUserInteractionEnabled = true editMemberAlert.deleteBtn.alpha = 1 } editMemberAlert.sureCallback = { [weak self] read, write, delete in guard let self = self else { return } LoadingView.show() NetworkManager.shared.shareFiles(paths: filePaths, usersId: userIds, read: read, write: write, delete: delete, fromUser: UserManager.shared.currentUser.nickname) { respond in SceneDelegate.shared.window?.makeToast("共享成功".localizedString) editMemberAlert.removeFromSuperview() LoadingView.hide() self.navigationController?.popViewController(animated: true) } failureCallback: { code, err in LoadingView.hide() SceneDelegate.shared.window?.makeToast("共享失败".localizedString) } } SceneDelegate.shared.window?.addSubview(editMemberAlert) }
#
2.4.3 文件上传下载#
2.4.3.1 文件上传选择器文件上传选择器中图片和视频的选择器采用的是第三方库TZImagePickerController,详细介绍及使用方法可参考TZImagePickerController,其他文件的选择器则采用系统自带的UIDocumentPickerViewController.
#
2.4.3.2 文件上传下载功能智汀云盘App的上传、下载功能是调用gomobile封装的库实现的.该库实现了文件的分片上传、分片下载、断点续传等功能,可以很方便的给客户端调用.iOS端基于该库封装了GoFileManager类,方便项目中使用.
GoFileManager类代码片段:
/// 下载任务 /// - Parameters: /// - url: 任务url func download(path: String) { goOperationQueue.async { [weak self] in guard let self = self else { return } print(Thread.current) /// 家庭scopeToken let scopeToken = AreaManager.shared.currentArea.scope_token /// 文件夹密码 let pwd = self.getDirDownloadPwd(by: path)
let downloader = GonetGetFileDownloader(self.downloadUrlFromPath(path), self.goCachePath, "{\"scope-token\":\"\(scopeToken)\", \"pwd\":\"\(pwd)\"}") self.taskCountChangePublisher.send(()) if self.getDownloadingCount() < 2 { downloader?.start(self.goDownloadCallbackObj) } } } /// 上传文件 /// - Parameters: /// - urlPath: 上传地址 /// - filename: 文件名 /// - tmpPath: 文件位置(图片、视频为绝对路径 其他文件为沙盒tmp下相对路径) /// - scopeToken: scopeToken func upload(urlPath: String, filename: String, tmpPath: String) { goOperationQueue.async { [weak self] in guard let self = self else { return } var filePath = tmpPath if let path = tmpPath.components(separatedBy: "file://").last { filePath = path } let scopeToken = AreaManager.shared.currentArea.scope_token let pwd = self.getDirUploadPwd(by: urlPath)
let uploader = GonetGetFileUploader("\(urlPath)\(filename)", self.goCachePath, filePath, filename, "{\"scope-token\":\"\(scopeToken)\", \"pwd\":\"\(pwd)\"}") try? uploader?.createTmpFile() self.taskCountChangePublisher.send(()) if self.getUploadingCount() < 2 { uploader?.start(self.goUploadCallbackObj) } } }
#
2.4.4 权限说明读:model类型的read为0则无可读权限,为1则有可读权限。
1、查看该文件夹及其下面所有文件/文件夹的权限; 2、有权限才展示对应文件夹的入口
写:model类型的write为0则无可写权限,为1则有可写权限。
1、包括新建文件夹、上传、重命名、共享、下载的权限; 2、有权限才展示对应操作的入口;
删:model类型的delete为0则无可删权限,为1则有可删权限。
1、包括移动、删除的权限;
2、有权限才展示对应的操作的入口;
3、有移动、复制的权限,不代表能成功移动复制,需要看是否有移入文件夹的【写】权限,有才能操作;
#
2.5 文件夹加密逻辑#
2.5.1 加密文件夹—加密过程1.文件夹的创建可设置私人文件夹及加密过程:
2.访问服务器接口,创建私人文件夹
NetworkManager.shared.createFolder(name: name, pool_name: pool_name, partition_name: partition_name, is_encrypt: is_encrypt, pwd: pwd, confirm_pwd: confirmPwd, mode: mode, auth: members) { [weak self] _ in self?.showToast("保存成功".localizedString) self?.navigationController?.popViewController(animated: true)
} failureCallback: { [weak self] code, err in self?.hideLoading() self?.showToast(err) }
3.创建给可访问用户后可在该用户的文件列表内展示。
#
2.5.2 加密文件夹—解密过程1.文件列表数据Model中属性is_encrypt
为 1 时,则为加密文件或文件夹
2.加密文件夹样式的区分。
3.加密文件夹进入的逻辑如下
- tableview点击相应代理方法,具体参考
MyFileViewController.swift
if file.is_encrypt == 1 {//加密文件 //存储的密码对象 let key = AreaManager.shared.currentArea.scope_token + file.path let pwdJsonStr:String = UserDefaults.standard.value(forKey: key) as? String ?? "" let pwdModel = PasswordModel.deserialize(from: pwdJsonStr) if pwdModel != nil { //计算时间差 let timeTemp = TimeTool.TimeInterval(FromTime: pwdModel!.saveTime) if timeTemp > 72 {//大于72小时 //重新输入密码 pushToFolder(isNeedPwd: true, file: file) }else{ //无需输入密码 pushToFolder(isNeedPwd: false, file: file) } }else{ //需要重新输入密码 pushToFolder(isNeedPwd: true, file: file) } }else{ //无需输入密码 pushToFolder(isNeedPwd: false, file: file) }
private func pushToFolder(isNeedPwd:Bool,file:FileModel){ let vc = FolderViewController() vc.currentPath = file.path vc.currentPaths = ["文件",file.name] vc.encrytRootFile = file vc.isWriteRoot = (file.write == 1) let key = AreaManager.shared.currentArea.scope_token + file.path if file.is_encrypt == 1 { vc.rootPasswordKey = key }else{ vc.rootPasswordKey = "" } if isNeedPwd { //存储文件夹根目录Key self.tipsTestFieldAlert = TipsTestFieldAlertView.show(message: "请输入密码", sureCallback: { pwd in print("密码是\(pwd)") NetworkManager.shared.decryptFolder(name: file.path, password: pwd) {[weak self] response in guard let self = self else {return} //存储时间和密码 let pwdModel = PasswordModel() pwdModel.password = pwd //当前时间 let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" pwdModel.saveTime = dateFormatter.string(from: Date()) UserDefaults.standard.setValue(pwdModel.toJSONString(prettyPrint:true), forKey: key) //解密成功,进入文件夹 let nav = BaseNavigationViewController(rootViewController: vc) nav.modalPresentationStyle = .fullScreen nav.transitioningDelegate = self.transitionUtil self.navigationController?.present(nav, animated: true, completion: nil) } failureCallback: { code, err in self.showToast(err) } }) }else{ //无需输入密码 let nav = BaseNavigationViewController(rootViewController: vc) nav.modalPresentationStyle = .fullScreen nav.transitioningDelegate = transitionUtil self.navigationController?.present(nav, animated: true, completion: nil) } }
- 加密文件夹进入子集目录需携带最初加密的文件,方便获取路径以及密码
- 解密过程逻辑如下:(其中规则根据自身项目的需求来定,本项目以超出72小时则自动失效为准则,所以需存储首次解密的时间)
//存储文件夹根目录Key self.tipsTestFieldAlert = TipsTestFieldAlertView.show(message: "请输入密码", sureCallback: { pwd in print("密码是\(pwd)") NetworkManager.shared.decryptFolder(name: file.path, password: pwd) {[weak self] response in guard let self = self else {return} //存储时间和密码 let pwdModel = PasswordModel() pwdModel.password = pwd //当前时间 let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" pwdModel.saveTime = dateFormatter.string(from: Date()) UserDefaults.standard.setValue(pwdModel.toJSONString(prettyPrint:true), forKey: key) //解密成功,进入文件夹 let nav = BaseNavigationViewController(rootViewController: vc) nav.modalPresentationStyle = .fullScreen nav.transitioningDelegate = self.transitionUtil self.navigationController?.present(nav, animated: true, completion: nil) } failureCallback: { code, err in self.showToast(err) } })
#
2.6 网络架构及临时通道#
2.6.1 网络架构的设计及使用的工具:Alamofire && Moya文件路径:Network/ApiService.swift
- ApiService 请求携带参数
enum ApiService { /// 创建目录 case createDirectory(area: Area, path: String, name: String, pwd: String = "") /// 删除文件/目录 case deleteFile(area: Area, paths: [String]) /// 文件/目录重命名 case renameFile(area: Area, path: String, name: String) /// 获取文件已上传的分块信息 case fileChunks(area: Area, hash: String) /// 目录下的文件/子目录列表 case fileList(area: Area, path: String, type:Int, page: Int = 1, page_size: Int = 30, pwd: String = "") //获取数据通道 case temporaryIP(area: Area, scheme: String = "http")}
- ApiService 请求IP地址(BaseUrl)
extension ApiService: TargetType { var baseURL: URL { switch self { case .deleteFile(let area, _), .renameFile(let area, _, _), .fileChunks(let area, _), .fileList(let area, _, _, _, _, _), return URL(string: "\(area.requestURL)")!
//临时通道接口需向云端服务器请求数据 case .temporaryIP: return URL(string:"云端服务器地址")! } }}
- ApiService 请求地址的拼接(Path)
var path: String {
switch self { case .deleteFile: return "/plugin/wangpan/resources" case .renameFile(_, let path, _): return "/plugin/wangpan/resources/\(path)" case .fileChunks(_, let hash): return "/plugin/wangpan/chunks/\(hash)" case .fileList(_, let path, _, _, _, _): return "/plugin/wangpan/resources/\(path)" case .temporaryIP: return "/datatunnel" } }
- ApiService 请求方式(Get,Post)
var method: Moya.Method { switch self { case .deleteFile: return .delete case .renameFile: return .put case .fileChunks: return .get case .fileList: return .get case .temporaryIP: return .get }}
- ApiService 创建Parameters Task
var task: Task { switch self { case .deleteFile(_, let paths): return .requestParameters(parameters: ["paths" : paths], encoding: JSONEncoding.default) case .renameFile(_, _, let name): return .requestParameters(parameters: ["name": name], encoding: JSONEncoding.default) case .fileChunks: return .requestPlain case .fileList(_, _, let type, let page ,let page_size, _): return .requestParameters(parameters: ["type": type, "page": page, "page_size": page_size], encoding: URLEncoding.default) case .temporaryIP(_, let scheme): return .requestParameters( parameters: [ "scheme": scheme ], encoding: URLEncoding.default ) }}
#
2.6.2 网络请求的构建扩展 Moya数据请求方式:
- 网络请求方法 extension MoyaProvider { ... }
/// 进行网络请求 /// - Parameters: /// - target: 请求的target /// - modelType: response解析的model /// - successCallback: 成功回调 /// - failureCallback: 失败回调 /// - Returns: 请求 @discardableResult func requestNetwork<T: BaseModel>(_ target: Target, modelType: T.Type, successCallback: ((_ response: T) -> Void)?, failureCallback: ((_ code: Int, _ errorMessage: String) -> Void)? = nil) -> Moya.Cancellable? { return request(target) { (result) in switch result { case .success(let response): if printDebugInfo { print("-----------------------------< ApiService >--------------------------------") print(Date()) print("---------------------------------------------------------------------------") print("header: \(target.headers ?? [:])") print("---------------------------------------------------------------------------") print("method: \(target.method.rawValue)") print("---------------------------------------------------------------------------") print("baseUrl: \(target.baseURL)") print("---------------------------------------------------------------------------") print("target: \(target.path)") print("---------------------------------------------------------------------------") print("parameters: \(target.task)") } guard response.statusCode == 200, let model = response.data.map(ApiServiceResponseModel<T>.self) else { failureCallback?(response.statusCode, "error: \(String(data: response.data, encoding: .utf8) ?? "unknown") code: \(response.statusCode)") print("---------------------------------------------------------------------------") print("error: \(String(data: response.data, encoding: .utf8) ?? "unknown")") print("---------------------------------------------------------------------------\n\n") return } if model.status == 0 { successCallback?(model.data) } else { failureCallback?(model.status, model.reason) } case .failure(let error): let moyaError = error as MoyaError let statusCode = moyaError.response?.statusCode ?? -1 let errorMessage = "error" failureCallback?(statusCode, errorMessage) return } } }
- 外部请求数据接口方法: 参考文件路径:Network/NetworkManager.swift
/// 删除文件或目录 /// - Parameters: /// - paths: 要删除的文件或目录 path数组 /// - successCallback: 成功回调 /// - failureCallback: 失败回调 /// - Returns: moya网络请求 func deleteFile(area: Area = AreaManager.shared.currentArea, paths: [String], successCallback: ((ExampleResponse) -> Void)?, failureCallback: ((Int, String) -> Void)?) { requestTemporaryIP(area: area) { [weak self] ip in guard let self = self else { return } //获取临时通道地址 if ip != "" { area.temporaryIP = "http://" + ip + "/api" } //请求结果 self.apiService.requestNetwork(.deleteFile(area : area, paths: paths), modelType: ExampleResponse.self, successCallback: successCallback, failureCallback: failureCallback) } failureCallback: { code, err in failureCallback?(code,err) } }
NetworkManager.shared.deleteFile(paths: paths) { response in //删除文件成功的回调 } failureCallback: { code, err in //删除文件失败的回调 }
#
2.6.3 临时通道含义及功能- 在非局域网的情况下,访问服务器需要走临时通道地址。
- 获取临时通道的方式如下:
//获取临时通道地址 private func requestTemporaryIP(area: Area = AreaManager.shared.currentArea, complete:((String)->())?, failureCallback: ((Int, String) -> ())?) { //在局域网内则直接连接局域网 if area.bssid == NetworkStateManager.shared.getWifiBSSID() && area.bssid != nil || !UserManager.shared.isCloudUser {//局域网 complete?("") GoFileNewManager.shared.updateHost(host: "\(AreaManager.shared.currentArea.sa_lan_address ?? "")/api") return } //不在SA局域网内且不是云端授权的情况,提示用户云端授权才可在外网使用 if area.bssid != NetworkStateManager.shared.getWifiBSSID() && area.bssid != nil && !UserManager.shared.isCloudUser { failureCallback?(-1, "智慧中心连接失败,请在智汀家庭云登录后重新授权连接或在局域网内连接") return } //获取本地存储的临时通道地址 let key = area.sa_user_token let temporaryJsonStr:String = UserDefaults.standard.value(forKey: key) as? String ?? "" let temporary = TemporaryResponse.deserialize(from: temporaryJsonStr) //验证是否过期,直接返回IP地址 if let temporary = temporary {//有存储信息 if timeInterval(fromTime: temporary.saveTime , second: temporary.expires_time) { //地址并未过期 GoFileNewManager.shared.updateHost(host: temporary.host) complete?(temporary.host) return } } //过期,请求服务器获取临时通道地址 apiService.requestNetwork(.temporaryIP(area: AreaManager.shared.currentArea, scheme: "http"), modelType: TemporaryResponse.self) { response in //获取临时通道地址及有效时间,存储在本地 //更新时间和密码 let temporaryModel = TemporaryResponse() temporaryModel.host = response.host temporaryModel.expires_time = response.expires_time //当前时间 let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" temporaryModel.saveTime = dateFormatter.string(from: Date()) UserDefaults.standard.setValue(temporaryModel.toJSONString(prettyPrint:true), forKey: key) //返回ip地址 complete?(response.host) GoFileNewManager.shared.updateHost(host: response.host)
} failureCallback: { code, error in failureCallback?(code,error) } }
- 临时通道根据下发的有效时间来校验,具体根据自身项目规则来定。
- 临时通道获取成功后,可以看到需替换掉Moyal请求的BaseUrl。
requestTemporaryIP(area: area) { [weak self] ip in guard let self = self else { return } //获取临时通道地址 if ip != "" { //替换当前的临时通道地址 area.temporaryIP = "http://" + ip + "/api" } //请求结果 self.apiService.requestNetwork(.deleteFile(area : area, paths: paths), modelType: ExampleResponse.self, successCallback: successCallback, failureCallback: failureCallback) } failureCallback: { code, err in //获取临时通道失败,反馈出去结果 failureCallback?(code,err) }
文件路径:Base/Model/Area.swift
var temporaryIP = "https://sc.zhitingtech.com/api" /// 请求的地址url(判断请求sa还是sc) var requestURL: URL { if bssid == NetworkStateManager.shared.getWifiBSSID() && bssid != nil {//局域网 return URL(string: "\(sa_lan_address ?? "")/api")! } else { return URL(string: temporaryIP)! } }
#
2.7 存储池及存储池分区#
2.7.1 存储池在我的页面,若该用户为拥有者,则可出现存储池入口,拥有者可操作存储池的权限。
文件路径:ThirdParty/StorageManage/StorageManageViewController.swift
- 列表数据根据服务器获取,顶部数据为SA的外接硬盘数据,下部分则为云端创建的存储池数据。
private func reloadData() { let semaphore = DispatchSemaphore(value: 1) if hardDisks.count == 0 && storagePools.count == 0 { showLoading(.custom(.white_ffffff)) } DispatchQueue.global().async { semaphore.wait() /// 获取限制硬盘列表 NetworkManager.shared.hardDiskList { [weak self] response in guard let self = self else { return } self.hardDisks = response.list semaphore.signal()
} failureCallback: { code, err in semaphore.signal() } /// 挂起任务,等待硬盘列表结果 semaphore.wait() /// 获取存储池列表 /// 传0直接获取全部数据 不分页 NetworkManager.shared.storagePoolList(page: 0, pageSize: 0) { [weak self] response in guard let self = self else { return } self.storagePools = response.list semaphore.signal() } failureCallback: { code, err in semaphore.signal() } /// 挂起任务,等待存储池列表结果 semaphore.wait() DispatchQueue.main.async { self.collectionView.mj_header?.endRefreshing() self.collectionView.reloadData() self.hideLoading() semaphore.signal() } } }
#
2.7.1.1 添加存储池文件路径:ThirdParty/StorageManage/AddToStoragePoolViewController.swift
/// 添加到新存储池 private func tapAddStorage() { print("点击添加到新的存储池") newStoragePoolAlert = SetNameAlertView(setNameType: .createStoragePool, currentName: "") newStoragePoolAlert?.setNameCallback = { [weak self] name in guard let self = self else { return } if name.isEmpty { SceneDelegate.shared.window?.makeToast("请输入名称".localizedString) return } if self.storagePools.map(\.name).contains(name) { SceneDelegate.shared.window?.makeToast("存储名称不能重复".localizedString) return } /// 请求添加存储池接口 LoadingView.show() NetworkManager.shared.addStoragePool(name: name, disk_name: self.disk_name) { [weak self] _ in guard let self = self else { return } LoadingView.hide() SceneDelegate.shared.window?.makeToast("添加成功") self.newStoragePoolAlert?.removeFromSuperview() self.navigationController?.popViewController(animated: true) } failureCallback: { [weak self] code, err in guard let self = self else { return } LoadingView.hide() if code == 205 { // 磁盘挂载失败 self.newStoragePoolAlert?.removeFromSuperview() let singleTipsAlert = SingleTipsAlertView(detail: "硬盘(\(self.disk_name))添加到存储池(\(name))失败,请重新添加。", detailColor: .custom(.black_3f4663), sureBtnTitle: "确定") singleTipsAlert.sureCallback = { [weak self] in guard let self = self else { return } singleTipsAlert.removeFromSuperview() self.navigationController?.popViewController(animated: true) } SceneDelegate.shared.window?.addSubview(singleTipsAlert) } else { SceneDelegate.shared.window?.makeToast(err) } } } SceneDelegate.shared.window?.addSubview(newStoragePoolAlert!) }
#
2.7.1.2 删除存储池 private func deletePool(){ let tipsAlert = TipsAlertView(title: "删除确认", detail: "确认删除该存储池吗?删除需要一些时间处理,且删除后,该存储池下的所有分区及其文件夹/文件都全部删除", warning: "操作不可撤销,请谨慎操作!", sureBtnTitle: "确定删除") tipsAlert.sureCallback = { [weak self] in guard let self = self else { return }
tipsAlert.sureBtn.buttonState = .waiting NetworkManager.shared.deleteStoragePool(name: self.currentStoragePoolName) {[weak self] response in guard let self = self else { return } tipsAlert.removeFromSuperview() //弹框提示后台处理 let singleTipsAlert = SingleTipsAlertView(detail: "正在删除存储池,已为您后台运行,可返回列表刷新查看。", detailColor: .custom(.black_3f4663), sureBtnTitle: "确定") singleTipsAlert.sureCallback = { [weak self] in guard let self = self else { return } singleTipsAlert.removeFromSuperview() self.navigationController?.popViewController(animated: true) } SceneDelegate.shared.window?.addSubview(singleTipsAlert)
// self.showToast("删除成功")// self.navigationController?.popViewController(animated: true) } failureCallback: { [weak self] code, err in guard let self = self else { return } self.showToast(err) tipsAlert.sureBtn.buttonState = .normal }
} SceneDelegate.shared.window?.addSubview(tipsAlert) }
#
2.7.2 存储池分区#
2.7.2.1 添加存储池分区 NetworkManager.shared.addPartition(name: nameTextFiled.text ?? "", capacity: Float(capacity) ?? 0, unit: capacityButton.titleLabel?.text ?? "GB", pool_name: currentStoragePoolName) {[weak self] response in LoadingView.hide() let singleTipsAlert = SingleTipsAlertView(detail: "正在保存分区信息,预计需要一些时间处理,已为您后台运行,可返回列表刷新查看", detailColor: .custom(.black_3f4663), sureBtnTitle: "确定") singleTipsAlert.sureCallback = { [weak self] in guard let self = self else { return } singleTipsAlert.removeFromSuperview() self.navigationController?.popViewController(animated: true) } SceneDelegate.shared.window?.addSubview(singleTipsAlert) } failureCallback: {[weak self] code, err in LoadingView.hide() self?.showToast(err) }
#
2.7.2.2 删除存储池分区 NetworkManager.shared.deletePartition(name: self.currentModel.name, pool_name: self.currentStoragePoolName) {[weak self] response in guard let self = self else {return} tipsAlert.sureBtn.buttonState = .normal tipsAlert.removeFromSuperview() //弹框提示后台处理 let singleTipsAlert = SingleTipsAlertView(detail: "正在删除分区,已为您后台运行,可返回 列表刷新查看。", detailColor: .custom(.black_3f4663), sureBtnTitle: "确定") singleTipsAlert.sureCallback = { [weak self] in guard let self = self else { return } singleTipsAlert.removeFromSuperview() self.navigationController?.popViewController(animated: true) } SceneDelegate.shared.window?.addSubview(singleTipsAlert) } failureCallback: {[weak self] code, err in guard let self = self else {return} tipsAlert.sureBtn.buttonState = .normal self.showToast(err) }
#
2.7.2.3 编辑存储池分区 judgeInfoChange(editName: nameTextFiled.text ?? "", editCapacity: capacity, unit: capacityButton.titleLabel?.text ?? "GB") {[weak self] capacityChanged, nameChanged, allowCapacity in guard let self = self else {return} if allowCapacity == 2 { LoadingView.hide() self.showToast("分区内存不能减少") return } if capacityChanged == 0 && nameChanged == 0{ LoadingView.hide() self.showToast("未修改任何内容") return } //保存编辑内容 NetworkManager.shared.editPartition(name: self.currentModel.name, new_name: self.nameTextFiled.text ?? "", pool_name: self.currentStoragePoolName, capacity: Float(capacity) ?? 0, unit: self.capacityButton.titleLabel?.text ?? "GB") {[weak self] response in LoadingView.hide() var showText = "" if nameChanged == 1 && capacityChanged == 0 {//仅编辑名称 showText = "保存成功" }else{ showText = "正在保存分区信息,需要一些时间处理,已为您后台运行,可返回列表刷新查看。" } let singleTipsAlert = SingleTipsAlertView(detail: showText, detailColor: .custom(.black_3f4663), sureBtnTitle: "确定") singleTipsAlert.sureCallback = { [weak self] in guard let self = self else { return } singleTipsAlert.removeFromSuperview() self.navigationController?.popViewController(animated: true) } SceneDelegate.shared.window?.addSubview(singleTipsAlert) } failureCallback: {[weak self] code, err in LoadingView.hide() self?.showToast(err) } }
#
2.8 文件夹管理#
2.8.1 文件夹列表: FolderManageViewController.swift /// 请求数据 private func requestData() { if folders.count == 0 { showLoading() } let page = (folders.count / 30) + 1 NetworkManager.shared.folderList(page: page, pageSize: 30) { [weak self] response in guard let self = self else { return } self.hideLoading() self.folders.append(contentsOf: response.list) self.collectionView.mj_header?.endRefreshing() self.collectionView.mj_footer?.endRefreshing() if !response.pager.has_more { self.collectionView.mj_footer?.endRefreshingWithNoMoreData() } self.collectionView.reloadData() } failureCallback: { [weak self] code, err in self?.collectionView.mj_header?.endRefreshing() self?.collectionView.mj_footer?.endRefreshing() self?.collectionView.reloadData() self?.hideLoading() self?.showToast(err) } } /// cell配置 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FolderManageCell.reusableIdentifier, for: indexPath) as! FolderManageCell let folder = folders[indexPath.row] cell.folder = folder /// menu按钮回调 cell.menuCallback = { [weak self] in guard let self = self, let cell = self.collectionView.cellForItem(at: indexPath) else { return } let x = 35.ztScaleValue let y = 50.ztScaleValue + Screen.k_nav_height let alertPoint = cell.convert(CGPoint(x: x, y: y), to: self.view) let alert = MenuAlert(items: [.init(title: "更改密码", icon: .assets(.icon_lock))], alertPoint: alertPoint) alert.selectCallback = { [weak self] item in guard let self = self else { return } if item.title == "更改密码" { let alert = FolderEditPwdAlert() alert.saveCallback = { [weak self] old, new, confrim in guard let self = self else { return } LoadingView.show() NetworkManager.shared.editFolderPwd(id: folder.id, oldPwd: old, newPwd: new, confirmPwd: confrim) { [weak self] _ in guard let self = self else { return } alert.removeFromSuperview() LoadingView.hide() SceneDelegate.shared.window?.makeToast("修改成功".localizedString) self.reloadData() } failureCallback: { code, err in LoadingView.hide() SceneDelegate.shared.window?.makeToast(err) } } SceneDelegate.shared.window?.addSubview(alert) } } SceneDelegate.shared.window?.addSubview(alert) } /// 状态cover 按钮回调 cell.statusCoverCallback = { [weak self] index in guard let self = self else { return } switch folder.statusEnum { case .failToDelete: if index == 0 { // 修改文件夹失败 - 重试 self.showLoading() NetworkManager.shared.restartAsyncTask(task_id: folder.task_id) { [weak self] _ in guard let self = self else { return } self.hideLoading() self.reloadData() } failureCallback: { [weak self] code, err in self?.hideLoading() self?.showToast(err) } } case .failToEdit: if index == 0 { // 修改文件夹失败 - 确定 self.showLoading() NetworkManager.shared.deleteAsyncTask(task_id: folder.task_id) { [weak self] _ in guard let self = self else { return } self.hideLoading() self.reloadData() } failureCallback: { [weak self] code, err in self?.hideLoading() self?.showToast(err) } } default: break } } return cell }
#
2.8.2 文件夹设置: FolderManageSettingViewController.swift /// 保存文件夹设置 @objc private func tapSave() { guard let pool = selectedPool, let partition = selectedPartition else { return }
showLoading() NetworkManager.shared.setFolderSettings(poolName: pool.name, partitionName: partition.name, autoDel: autoDeleteCell.switchBtn.isOn) { [weak self] _ in self?.hideLoading() SceneDelegate.shared.window?.makeToast("保存成功".localizedString) self?.navigationController?.popViewController(animated: true) } failureCallback: { [weak self] code, err in self?.hideLoading() self?.showToast(err) } } /// 获取文件夹设置 private func getFolderSettings() { showLoading(.custom(.white_ffffff)) let sp = DispatchSemaphore(value: 1) DispatchQueue.global().async { sp.wait() /// 1获取存储池列表 NetworkManager.shared.storagePoolList(page: 0, pageSize: 0) { [weak self] response in guard let self = self else { return } self.storagePools = response.list sp.signal()
} failureCallback: { _, _ in sp.signal() } sp.wait() NetworkManager.shared.getFolderSettings { [weak self] settings in sp.signal() guard let self = self else { return } self.hideLoading() self.autoDeleteCell.switchBtn.setIsOn(settings.is_auto_del) self.storageDefaultCell.nameLabel.text = "\(settings.pool_name)-\(settings.partition_name)" self.selectedPool = self.storagePools.first(where: { $0.name == settings.pool_name }) if let selectedPool = self.selectedPool { self.selectedPartition = selectedPool.lv.first(where: { $0.name == settings.partition_name }) } } failureCallback: { [weak self] code, err in sp.signal() self?.hideLoading() self?.showToast(err) } } }
#
2.8.3 创建、编辑、删除文件夹:EditFolderViewController.swift /// 删除文件夹 @objc private func tapDelete() { let alert = TipsAlertView(title: "确定删除该文件夹吗?", detail: "删除后,该文件夹及其包含的所有文件夹/文件都全部删除。", warning: "操作不可撤销,请谨慎操作!", sureBtnTitle: "确认删除") alert.sureCallback = { [weak self] in guard let self = self else { return } guard let id = self.type.folderId else { return }
LoadingView.show() NetworkManager.shared.deleteFolder(id: id) { [weak self] _ in LoadingView.hide() self?.showToast("删除成功".localizedString) alert.removeFromSuperview() self?.navigationController?.popViewController(animated: true) } failureCallback: { [weak self] code, err in LoadingView.hide() self?.showToast(err) alert.removeFromSuperview() } } SceneDelegate.shared.window?.addSubview(alert) } /// 保存修改 @objc private func save() { if type == .create { let name = nameCell.textField.text ?? "" let pool_name = selectedPool?.name ?? "" let partition_name = selectedPartition?.name ?? "" let is_encrypt = isSecureCell.selectView.selectedIndex == 0 ? 1 : 0 let pwd = pwdCell1.textField.text ?? "" let confirmPwd = pwdCell2.textField.text ?? "" var mode: FolderModel.FolderMode = .shared if folderTypeCell.selectView.selectedIndex == 0 { mode = .private } showLoading() NetworkManager.shared.createFolder(name: name, pool_name: pool_name, partition_name: partition_name, is_encrypt: is_encrypt, pwd: pwd, confirm_pwd: confirmPwd, mode: mode, auth: members) { [weak self] _ in self?.showToast("保存成功".localizedString) self?.navigationController?.popViewController(animated: true)
} failureCallback: { [weak self] code, err in self?.hideLoading() self?.showToast(err) } } else { guard let folder = folder else { return } let name = nameCell.textField.text ?? "" let pool_name = selectedPool?.name ?? "" let partition_name = selectedPartition?.name ?? "" var mode: FolderModel.FolderMode = .shared if folderTypeCell.selectView.selectedIndex == 0 { mode = .private } //如果修改了分区 if pool_name != folder.pool_name || partition_name != folder.partition_name { let tipsAlert = TipsAlertView(title: "存储分区转移".localizedString, titleColor: .custom(.black_3f4663), detail: "\(name)存储分区从“\(folder.pool_name ?? "")-\(folder.partition_name ?? "")”改为“\(pool_name)-\(partition_name)”", detailColor: .custom(.black_3f4663), warning: "修改预计需要一段时间处理,且中途不可取消。确定要修改吗?".localizedString, warningColor: .custom(.red_fe0000), sureBtnTitle: "确定".localizedString) tipsAlert.sureCallback = { [weak self] in guard let self = self else { return } self.editFolder(id: folder.id, name: name, pool_name: pool_name, partition_name: partition_name, is_encrypt: folder.is_encrypt, mode: mode, auth: self.members) tipsAlert.removeFromSuperview() } SceneDelegate.shared.window?.addSubview(tipsAlert) } else { editFolder(id: folder.id, name: name, pool_name: pool_name, partition_name: partition_name, is_encrypt: folder.is_encrypt, mode: mode, auth: members) } } } /// 编辑文件夹 private func editFolder(id: Int, name: String, pool_name: String, partition_name: String, is_encrypt: Int, mode: FolderModel.FolderMode, auth: [User]) { showLoading() NetworkManager.shared.editFolder(id: id, name: name, pool_name: pool_name, partition_name: partition_name, is_encrypt: is_encrypt, mode: mode, auth: auth) { [weak self] _ in guard let self = self else { return } if pool_name != self.folder?.pool_name || partition_name != self.folder?.partition_name { //如果修改了分区 let alert = SingleTipsAlertView(detail: "存储分区转移" + "\n\n" + "\(name)存储分区正在从“\(self.folder?.pool_name ?? "")-\(self.folder?.partition_name ?? "")”改为“\(pool_name)-\(partition_name)”,已为您后台运行,可返回列表查看。", sureBtnTitle: "确定".localizedString) alert.sureCallback = { [weak self] in guard let self = self else { return } alert.removeFromSuperview() self.showToast("保存成功".localizedString) self.navigationController?.popViewController(animated: true) } SceneDelegate.shared.window?.addSubview(alert)
} else { self.showToast("保存成功".localizedString) self.navigationController?.popViewController(animated: true) } } failureCallback: { [weak self] code, err in guard let self = self else { return } self.hideLoading() if code == 20019 { //目标分区容量不足,不能迁移 let errAlert = SingleTipsAlertView(detail: "存储分区修改失败".localizedString + "\n\n" + "分区容量不足!".localizedString, sureBtnTitle: "确定".localizedString) errAlert.sureCallback = { errAlert.removeFromSuperview() } SceneDelegate.shared.window?.addSubview(errAlert) } else { self.showToast(err) } } }