背景
在iOS端由于文件系統(tǒng)的封閉性鸽捻,文件的上傳變得十分麻煩片仿,一個比較好的解決方案是通過局域網(wǎng)WiFi來傳輸文件并存儲到沙盒中。
簡介
SGWiFiUpload是一個基于CocoaHTTPServer的WiFi上傳框架。CocoaHTTPServer是一個可運行于iOS和OS X上的輕量級服務(wù)端框架,可以處理GET和POST請求闽颇,通過對代碼的初步改造,實現(xiàn)了iOS端的WiFi文件上傳與上傳狀態(tài)監(jiān)聽寄锐。
下載與使用
目前已經(jīng)做成了易用的框架兵多,上傳到了GitHub尖啡,點擊這里進入,歡迎Star剩膘!
請求的處理
CocoaHTTPServer通過HTTPConnection這一接口實現(xiàn)類來回調(diào)網(wǎng)絡(luò)請求的各個狀態(tài)衅斩,包括對請求頭、響應(yīng)體的解析等援雇。為了實現(xiàn)文件上傳矛渴,需要自定義一個繼承HTTPConnection的類,這里命名為SGHTTPConnection
惫搏,與文件上傳有關(guān)的幾個方法如下。
解析文件上傳的請求頭
- (void)processStartOfPartWithHeader:(MultipartMessageHeader*) header {
// in this sample, we are not interested in parts, other then file parts.
// check content disposition to find out filename
MultipartMessageHeaderField* disposition = [header.fields objectForKey:@"Content-Disposition"];
NSString* filename = [[disposition.params objectForKey:@"filename"] lastPathComponent];
if ( (nil == filename) || [filename isEqualToString: @""] ) {
// it's either not a file part, or
// an empty form sent. we won't handle it.
return;
}
// 這里用于發(fā)出文件開始上傳的通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SGFileUploadDidStartNotification object:@{@"fileName" : filename ?: @"File"}];
});
// 這里用于設(shè)置文件的保存路徑蚕涤,先預(yù)存一個空文件筐赔,然后進行追加寫內(nèi)容
NSString *uploadDirPath = [SGWiFiUploadManager sharedManager].savePath;
BOOL isDir = YES;
if (![[NSFileManager defaultManager]fileExistsAtPath:uploadDirPath isDirectory:&isDir ]) {
[[NSFileManager defaultManager]createDirectoryAtPath:uploadDirPath withIntermediateDirectories:YES attributes:nil error:nil];
}
NSString* filePath = [uploadDirPath stringByAppendingPathComponent: filename];
if( [[NSFileManager defaultManager] fileExistsAtPath:filePath] ) {
storeFile = nil;
}
else {
HTTPLogVerbose(@"Saving file to %@", filePath);
if(![[NSFileManager defaultManager] createDirectoryAtPath:uploadDirPath withIntermediateDirectories:true attributes:nil error:nil]) {
HTTPLogError(@"Could not create directory at path: %@", filePath);
}
if(![[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]) {
HTTPLogError(@"Could not create file at path: %@", filePath);
}
storeFile = [NSFileHandle fileHandleForWritingAtPath:filePath];
[uploadedFiles addObject: [NSString stringWithFormat:@"/upload/%@", filename]];
}
}
其中有中文注釋的兩處是比較重要的地方,這里根據(jù)請求頭發(fā)出了文件開始上傳的通知揖铜,并且往要存放的路徑寫一個空文件茴丰,以便后續(xù)追加內(nèi)容。
上傳過程中的處理
- (void) processContent:(NSData*) data WithHeader:(MultipartMessageHeader*) header
{
// here we just write the output from parser to the file.
// 由于除去文件內(nèi)容外天吓,還有HTML內(nèi)容和空文件通過此方法處理贿肩,因此需要過濾掉HTML和空文件內(nèi)容
if (!header.fields[@"Content-Disposition"]) {
return;
} else {
MultipartMessageHeaderField *field = header.fields[@"Content-Disposition"];
NSString *fileName = field.params[@"filename"];
if (fileName.length == 0) return;
}
self.currentLength += data.length;
CGFloat progress;
if (self.contentLength == 0) {
progress = 1.0f;
} else {
progress = (CGFloat)self.currentLength / self.contentLength;
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SGFileUploadProgressNotification object:@{@"progress" : @(progress)}];
});
if (storeFile) {
[storeFile writeData:data];
}
}
這里除了拼接文件內(nèi)容以外,還發(fā)出了上傳進度的通知龄寞,當(dāng)前方法中只能拿到這一段文件的長度汰规,總長度需要通過下面的方法拿到。
獲取文件大小
- (void)prepareForBodyWithSize:(UInt64)contentLength
{
HTTPLogTrace();
// 設(shè)置文件總大小物邑,并初始化當(dāng)前已經(jīng)傳輸?shù)奈募笮 ? self.contentLength = contentLength;
self.currentLength = 0;
// set up mime parser
NSString* boundary = [request headerField:@"boundary"];
parser = [[MultipartFormDataParser alloc] initWithBoundary:boundary formEncoding:NSUTF8StringEncoding];
parser.delegate = self;
uploadedFiles = [[NSMutableArray alloc] init];
}
處理傳輸完畢
- (void) processEndOfPartWithHeader:(MultipartMessageHeader*) header
{
// as the file part is over, we close the file.
// 由于除去文件內(nèi)容外溜哮,還有HTML內(nèi)容和空文件通過此方法處理,因此需要過濾掉HTML和空文件內(nèi)容
if (!header.fields[@"Content-Disposition"]) {
return;
} else {
MultipartMessageHeaderField *field = header.fields[@"Content-Disposition"];
NSString *fileName = field.params[@"filename"];
if (fileName.length == 0) return;
}
[storeFile closeFile];
storeFile = nil;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SGFileUploadDidEndNotification object:nil];
});
}
這里關(guān)閉了文件管道色解,并且發(fā)出了文件上傳完畢的通知茂嗓。
開啟Server
CocoaHTTPServer默認的Web根目錄為MainBundle,他會在目錄下尋找index.html科阎,文件上傳的請求地址為upload.html述吸,當(dāng)以POST方式請求upload.html時,請求會被Server攔截锣笨,并且交由HTTPConnection處理蝌矛。
- (BOOL)startHTTPServerAtPort:(UInt16)port {
HTTPServer *server = [HTTPServer new];
server.port = port;
self.httpServer = server;
[self.httpServer setDocumentRoot:self.webPath];
[self.httpServer setConnectionClass:[SGHTTPConnection class]];
NSError *error = nil;
[self.httpServer start:&error];
return error == nil;
}
在HTML中發(fā)送POST請求上傳文件
在CocoaHTTPServer給出的樣例中有用于文件上傳的index.html,要實現(xiàn)文件上傳票唆,只需要一個POST方法的form表單朴读,action為upload.html,每一個文件使用一個input標(biāo)簽走趋,type為file即可衅金,這里為了美觀對input標(biāo)簽進行了自定義。
下面的代碼演示了能同時上傳3個文件的index.html代碼。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">
</head>
<style>
body {
margin: 0px;
padding: 0px;
font-size: 12px;
background-color: rgb(244,244,244);
text-align: center;
}
#container {
margin: auto;
}
#form {
margin-top: 60px;
}
.upload {
margin-top: 2px;
}
#submit input {
background-color: #ea4c88;
color: #eee;
font-weight: bold;
margin-top: 10px;
text-align: center;
font-size: 16px;
border: none;
width: 120px;
height: 36px;
}
#submit input:hover {
background-color: #d44179;
}
#submit input:active {
background-color: #a23351;
}
.uploadField {
margin-top: 2px;
width: 200px;
height: 22px;
font-size: 12px;
}
.uploadButton {
background-color: #ea4c88;
color: #eee;
font-weight: bold;
text-align: center;
font-size: 15px;
border: none;
width: 80px;
height: 26px;
}
.uploadButton:hover {
background-color: #d44179;
}
.uploadButton:active {
background-color: #a23351;
}
</style>
<body>
<div id="container">
<div id="form">
<h2>WiFi File Upload</h2>
<form name="form" action="upload.html" method="post" enctype="multipart/form-data" accept-charset="utf-8">
<div class="upload">
<input type="file" name="upload1" id="upload1" style="display:none" onChange="document.form.path1.value=this.value">
<input class="uploadField" name="path1" readonly>
<input class="uploadButton" type="button" value="Open" onclick="document.form.upload1.click()">
</div>
<div class="upload">
<input type="file" name="upload2" id="upload2" style="display:none" onChange="document.form.path2.value=this.value">
<input class="uploadField" name="path2" readonly>
<input class="uploadButton" type="button" value="Open" onclick="document.form.upload2.click()">
</div>
<div class="upload">
<input type="file" name="upload3" id="upload3" style="display:none" onChange="document.form.path3.value=this.value">
<input class="uploadField" name="path3" readonly>
<input class="uploadButton" type="button" value="Open" onclick="document.form.upload3.click()">
</div>
<div id="submit"><input type="submit" value="Submit"></div>
</form>
</div>
</div>
</body>
</html>
表單提交后氮唯,會進入upload.html頁面鉴吹,該頁面用于說明上傳完畢,下面的代碼實現(xiàn)了3秒后的重定向返回惩琉。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">
<meta http-equiv=refresh content="3;url=index.html">
</head>
<body>
<h3>Upload Succeeded!</h3>
<p>The Page will be back in 3 seconds</p>
</body>
</html>