前言
首先,我是一个Android开发者,这也是我第一次用Swift写东西,所以可能会有并不太地道的用法,请见谅。先看一下软件基本信息:
- 开发语言:Swift 5
- 操作系统:macOS 10.13及以上
- 功能:在同一个网络下,生成文件对应的二维码及链接,提供给其他设备进行下载
- 形式:GUI
看一下演示效果:(压缩得有点狠,将就看一看)
功能分析
- 本地开启http服务
- 编写接口将文件写入流
- 添加GUI实现
本地开启http服务
通过各种搜索工具,我找到了一个叫做Perfect的库。同时,在查看Perfect文档的同时,我也发现Perfect做了Sqlite的ORM,所以也就一起拿来用了。本来是准备做常驻服务来着,所以就做了数据库来存东西,但是后来觉得不太方便使用,又去掉了,现在只是当作缓存在使用。
首先看一下两个表:tb_config和tb_share.
tb_config
key | type | remark |
---|---|---|
key | String | 主键 |
value | String | 配置参数 |
remark | String | 备注 |
tb_share
key | type | remark |
---|---|---|
id | Int | 主键 |
name | String | 文件名 |
key | String | 文件标识,用于展示分享链接 |
path | String | 文件路径 |
createTime | Int64 | 创建时间 |
数据库差不多就这样,看一下tb_share
里面key的生成,其实就是时间戳加上文件名取md5之后再base64。能得到一个类似这样的结果:h-rdq6pojZWQcCr0j0kRAg
//
// DataUtils.swift
// EasyShare
//
// Created by Michael Lee on 2020/5/4.
// Copyright © 2020 Michael Lee. All rights reserved.
//
import PerfectCrypto
import Foundation
class DataUtils{
static func generateKey(name :String) -> String{
let md5 = "\(NSDate().timeIntervalSince1970)\(name)".digest(.md5)?.encode(.base64url)
return String(validatingUTF8: md5!) ?? "\(NSDate().timeIntervalSince1970)\(name)"
}
}
编写接口将文件写入流
现在通过Perfect创建本地服务。
//
// Server.swift
// EasyShare
//
// Created by Michael Lee on 2020/5/3.
// Copyright © 2020 Michael Lee. All rights reserved.
//
import Foundation
import PerfectHTTP
import PerfectLib
import PerfectHTTPServer
class ShareServer{
static let instance = ShareServer()
init() {
//添加api路由
addApi()
//添加web路由,本来是准备做h5展示再点击下载,后来觉得没必要,就没做
addWeb()
routes.add(api)
routes.add(web)
}
//标记服务是否开启
var state = 0
//默认端口号
var port :UInt16 = 8899
let queue = DispatchQueue.global()
var httpServer = HTTPServer()
var routes = Routes()
var api = Routes(baseUri: "/api")
var web = Routes(baseUri: "/web")
/// 开启服务
func start(){
if state == 1 {
return
}
state = 1
httpServer.serverName = "EasyShare"
httpServer.addRoutes(routes)
do{
try httpServer.serverPort = UInt16(DbHelper.getConfig(key: ConfigMap.port).value) ?? port
}catch{
httpServer.serverPort = port
}
port = httpServer.serverPort
queue.async {
do{
try self.httpServer.start()
}catch{
self.state = 0
fatalError("\(error)")
}
}
}
/// 停止服务
func stop(){
httpServer.stop()
state = 0
}
func addApi() {
/// 获取分享详情
api.add(method: .get, uri: "/info/{key}", handler: {request,response in
let key = request.urlVariables["key"]
let share = ShareDTO()
do {
try share.find([("key",key!)])
}catch{
do{
try response.setBody(json: self.createResponseBody(code: 500, message: "key error", data: nil))
}catch{
response.setBody(string: "\(error)")
}
response.completed()
return
}
if(share.id == 0){
do{
try response.setBody(json: self.createResponseBody(code: 404, message: "key not found", data: nil))
}catch{
response.setBody(string: "\(error)")
}
response.completed()
}else{
do{
try response.setBody(json:
self.createResponseBody(
code: 200,
message: "success",
data: share.asDataDict()))
}catch{
response.setBody(string: "\(error)")
}
response.completed()
}
})
/// 下载
api.add(method: .get, uri: "/download/{key}", handler: {request,response in
let share = ShareDTO()
let key = request.urlVariables["key"]
do {
try share.find([("key",key!)])
}catch{
do{
try response.setBody(json: self.createResponseBody(code: 500, message: "key error", data: nil))
}catch{
response.setBody(string: "\(error)")
}
response.completed()
return
}
if(share.id == 0){
do{
try response.setBody(json: self.createResponseBody(code: 404, message: "key not found", data: nil))
}catch{
response.setBody(string: "\(error)")
}
response.completed()
return
}
//获取文件
let file = File(share.path + "/" + share.name)
if(file.exists && !file.isDir){
do{
try file.open()
let size = file.size
let contentType = MimeType.forExtension(file.path.filePathExtension)
response.status = .ok
response.isStreaming = true
response.setHeader(.contentType, value: contentType)
response.setHeader(.contentLength, value: "\(size)")
response.setHeader(.acceptRanges, value: "bytes")
response.setHeader(.contentDisposition, value: "attachment;filename=\"\(share.name)\"")
self.pushBody(response: response, file: file)
}catch{
response.setBody(string: "\(error)")
response.completed()
}
}else{
//文件不存在
do{
try response.setBody(json: self.createResponseBody(code: 404, message: "file not exists", data: nil))
}catch{
response.setBody(string: "\(error)")
}
response.completed()
}
})
}
/// 往response里面写流
func pushBody(response:HTTPResponse,file:File){
let readSize = 5 * 1024 * 1024//每次读5m
var bytes :[UInt8]
do {
bytes = try file.readSomeBytes(count: readSize)
}catch{
bytes = [UInt8]()
}
if(bytes.count==0){
file.close()
response.completed()
return
}
response.appendBody(bytes: bytes)
response.push(callback: { bool in
if(bool){
self.pushBody(response: response, file: file)
}else{
file.close()
response.completed(status: HTTPResponseStatus.gatewayTimeout)
}
})
}
func addWeb() {
}
/// 生成json返回值
func createResponseBody(code:Int,message:String,data:Any?) -> [String:Any] {
return ["code":code,"message":message,"data":data ?? [String:Any]()]
}
}
以上就是本地服务的所有代码,本来东西很少,所以就没有分开了。
添加GUI实现
我找了很久,终于找到一个叫做Share Extension的东西,就是会显示在分享菜单里面,但是这个东西怎么用,根本就没有文档,而且网上找出来全部是iOS相关的东西。这里我就自己摸索着写的。
//
// ShareViewController.swift
// ShareExtension
//
// Created by Michael Lee on 2020/5/4.
// Copyright © 2020 Michael Lee. All rights reserved.
//
import Cocoa
import PerfectLib
import SwiftUI
class ShareViewController: NSViewController {
@IBOutlet weak var titleCell: NSTextFieldCell!
@IBOutlet weak var imageCell: NSImageCell!
@IBOutlet weak var urlCell: NSTextFieldCell!
var id = 0
var url = ""
override var nibName: NSNib.Name? {
return NSNib.Name("ShareViewController")
}
override func loadView() {
super.loadView()
// 这个可以获取到分享是点的哪里
let provider = (self.extensionContext!.inputItems[0] as! NSExtensionItem).attachments?[0]
// 获取URL对象
provider?.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: {data,error in
let url = data as! NSURL
// 获取绝对路径
let path = url.absoluteString
if(path!.starts(with: "file://")){
//走文件分享的方法
self.shareFile(path: url.path!)
}else if(path!.starts(with: "http://") || path!.starts(with: "https://")){
//分享网页链接
self.shareWeb(path: path!)
}else{
//都不是,则直接结束
self.extensionContext!.completeRequest(returningItems: [NSExtensionItem()], completionHandler: nil)
}
})
}
func shareFile(path:String) {
// 初始化数据库
DbHelper.create()
// 开启本地服务
ShareServer.instance.start()
let file = File(path)
if(file.exists){
// 开始写入数据库
let splits = file.path.split(separator: "/")
let name = splits[splits.count-1]
let share = ShareDTO()
share.name = "\(name)"
share.path = file.path.replacingOccurrences(of: "/\(name)", with: "")
share.key = DataUtils.generateKey(name: share.name)
do{
try share.save{ id in
share.id = id as! Int
self.id = share.id
}
}catch{
fatalError("\(error)")
}
// 这一句只是打印一下数据库中所有的分享,调试用的,没啥实质用处
self.findAllShare()
// 显示窗口信息
self.showWindowInfo(
url: "http://\(self.getIFAddresses()[0]):\(ShareServer.instance.port)/api/download/\(share.key)",
title: share.name
)
}else{
self.extensionContext!.completeRequest(returningItems: [NSExtensionItem()], completionHandler: nil)
}
}
func shareWeb(path:String){
// 直接显示窗口信息
self.showWindowInfo(url: path, title: "我是真的不知道Title怎么获取😅👌")
}
func showWindowInfo(url:String,title:String) {
self.url = url
self.urlCell.stringValue = url
self.titleCell.stringValue = title
self.imageCell.image = self.generateQRCodeImage(self.url, size: NSSize(width: 600, height: 600))
}
@IBAction func copy(_ sender: NSButton) {
// 拷贝到粘贴板
let pasteboard = NSPasteboard.general
pasteboard.declareTypes([NSPasteboard.PasteboardType.string], owner: nil)
let b = pasteboard.setString(url, forType: NSPasteboard.PasteboardType.string)
print(b)
}
@IBAction func send(_ sender: AnyObject?) {
// 结束分享
ShareServer.instance.stop()
let share = ShareDTO()
do{
try share.delete(id)
}catch{
NSLog("\(error)")
}
self.extensionContext!.completeRequest(returningItems: [NSExtensionItem()], completionHandler: nil)
}
/// 生成二维码
func generateQRCodeImage(_ content: String, size: NSSize) -> NSImage?{
// 创建滤镜
guard let filter = CIFilter(name: "CIQRCodeGenerator") else {return nil}
// 还原滤镜的默认属性
filter.setDefaults()
//1.3 设置生成的二维码的容错率
//value = @"L/M/Q/H"
filter.setValue("L", forKey: "inputCorrectionLevel")
// 设置需要生成的二维码数据
let contentData = content.data(using: String.Encoding.utf8)
filter.setValue(contentData, forKey: "inputMessage")
// 从滤镜中取出生成的图片
guard let ciImage = filter.outputImage else {return nil}
let context = CIContext(options: nil)
let bitmapImage = context.createCGImage(ciImage, from: ciImage.extent)
let colorSpace = CGColorSpaceCreateDeviceGray()
let bitmapContext = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: CGImageAlphaInfo.none.rawValue)
//draw image
let scale = min(size.width / ciImage.extent.width, size.height / ciImage.extent.height)
bitmapContext!.interpolationQuality = CGInterpolationQuality.none
bitmapContext?.scaleBy(x: scale, y: scale)
bitmapContext?.draw(bitmapImage!, in: ciImage.extent)
//保存bitmap到图片
guard let scaledImage = bitmapContext?.makeImage() else {return nil}
return NSImage(cgImage: scaledImage, size: size)
}
/// 获取本地IP地址
func getIFAddresses() -> [String] {
var addresses = [String]()
// Get list of all interfaces on the local machine:
var ifaddr : UnsafeMutablePointer<ifaddrs>? = nil
if getifaddrs(&ifaddr) == 0 {
var ptr = ifaddr
while ptr != nil {
let flags = Int32((ptr?.pointee.ifa_flags)!)
var addr = ptr?.pointee.ifa_addr.pointee
// Check for running IPv4, IPv6 interfaces. Skip the loopback interface.
if (flags & (IFF_UP|IFF_RUNNING|IFF_LOOPBACK)) == (IFF_UP|IFF_RUNNING) {
if addr?.sa_family == UInt8(AF_INET) && addr?.sa_family != UInt8(AF_INET6) {
// Convert interface address to a human readable string:
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
if (getnameinfo(&addr!, socklen_t((addr?.sa_len)!), &hostname, socklen_t(hostname.count),
nil, socklen_t(0), NI_NUMERICHOST) == 0) {
if let address = String(validatingUTF8: hostname) {
addresses.append(address)
}
}
}
}
ptr = ptr?.pointee.ifa_next
}
freeifaddrs(ifaddr)
}
print("Local IP \(addresses)")
return addresses
}
func findAllShare(){
let share = ShareDTO()
do{
try share.findAll()
let rows = share.rows()
for row in rows{
print(row.asDataDict())
}
}catch{
fatalError("\(error)")
}
}
}
收个尾
主要代码都在上面了,其实就两个主要的类。贴个GitHub地址,欢迎各位点星。
GitHub
要直接用的也可以在release里面直接下载,有任何意见或建议欢迎提issue。