1
1
import MultipartKit
2
2
import Papyrus
3
+ import NIOCore
3
4
4
- /// Represents a file with a name and binary contents.
5
+ // File
5
6
public struct File : Codable , ResponseConvertible {
6
- // The name of the file, including the extension.
7
+ public enum Source {
8
+ // The file is stored in a `Filesystem` with the given path.
9
+ case filesystem( Filesystem ? = nil , path: String )
10
+ // The file came with the given ContentType from an HTTP request.
11
+ case http( clientContentType: ContentType ? )
12
+
13
+ static var raw : Source {
14
+ . http( clientContentType: nil )
15
+ }
16
+ }
17
+
18
+ /// The name of this file, including the extension
7
19
public var name : String
8
- // The size of the file, in bytes.
9
- public let size : Int
10
- // The binary contents of the file.
11
- public var content : ByteContent
20
+ /// The source of this file, either from an HTTP request or from a Filesystem.
21
+ public var source : Source
22
+ public var content : ByteContent ?
23
+ public let size : Int ?
24
+ public let clientContentType : ContentType ?
12
25
/// The path extension of this file.
13
- public var `extension` : String { name . components ( separatedBy : " . " ) . last ?? " " }
14
- /// The content type of this file, based on it's extension.
15
- public let contentType : ContentType
26
+ public var `extension` : String {
27
+ name . components ( separatedBy : " . " ) . last ?? " "
28
+ }
16
29
17
- public init ( name: String , contentType: ContentType ? = nil , size: Int , content: ByteContent ) {
30
+ public var contentType : ContentType {
31
+ name. components ( separatedBy: " . " ) . last. map { ContentType ( fileExtension: $0) ?? . octetStream } ?? . octetStream
32
+ }
33
+
34
+ public init ( name: String , source: Source , content: ByteContent ? = nil , size: Int ? = nil ) {
18
35
self . name = name
19
- self . size = size
36
+ self . source = source
20
37
self . content = content
21
- let _extension = name . components ( separatedBy : " . " ) . last ?? " "
22
- self . contentType = contentType ?? ContentType ( fileExtension : _extension ) ?? . octetStream
38
+ self . size = size
39
+ self . clientContentType = nil
23
40
}
24
41
25
- /// Returns a copy of this file with a new name.
26
- public func named( _ name: String ) -> File {
42
+ public func _in( _ filesystem: Filesystem ) -> File {
27
43
var copy = self
28
- copy. name = name
44
+ switch source {
45
+ case . filesystem( _, let path) :
46
+ copy. source = . filesystem( filesystem, path: path)
47
+ default :
48
+ break
49
+ }
50
+
29
51
return copy
30
52
}
31
53
54
+ // MARK: - Accessing Contents
55
+
56
+ /// get a url for this resource
57
+ public func url( ) throws -> URL {
58
+ switch source {
59
+ case . filesystem( let filesystem, let path) :
60
+ return try ( filesystem ?? Storage) . url ( path)
61
+ case . http:
62
+ throw FileError . urlUnavailable
63
+ }
64
+ }
65
+
66
+ /// get temporary url for this resource
67
+ public func temporaryUrl( expires: TimeAmount , headers: HTTPHeaders = [ : ] ) async throws -> URL {
68
+ switch source {
69
+ case . filesystem( let filesystem, let path) :
70
+ return try await ( filesystem ?? Storage) . temporaryURL ( path, expires: expires, headers: headers)
71
+ default :
72
+ throw FileError . temporaryUrlNotAvailable
73
+ }
74
+ }
75
+
76
+ public func getContent( ) async throws -> ByteContent {
77
+ guard let content = content else {
78
+ switch source {
79
+ case . http:
80
+ throw FileError . contentNotLoaded
81
+ case . filesystem( let filesystem, let path) :
82
+ return try await ( filesystem ?? Storage) . get ( path) . getContent ( )
83
+ }
84
+ }
85
+
86
+ return content
87
+ }
88
+
89
+ // MARK: ModelProperty
90
+
91
+ init ( key: String , on row: SQLRowReader ) throws {
92
+ let name = try row. require ( key) . string ( )
93
+ self . init ( name: name, source: . filesystem( Storage, path: name) )
94
+ }
95
+
96
+ func store( key: String , on row: inout SQLRowWriter ) throws {
97
+ guard case . filesystem( _, let path) = source else {
98
+ throw RuneError ( " currently, only files saved in a `Filesystem` can be stored on a `Model` " )
99
+ }
100
+
101
+ row. put ( . string( path) , at: key)
102
+ }
103
+
32
104
// MARK: - ResponseConvertible
33
105
34
106
public func response( ) async throws -> Response {
35
- Response ( status: . ok, headers: [ " Content-Disposition " : " inline; filename= \" \( name) \" " ] )
107
+ let content = try await getContent ( )
108
+ return Response ( status: . ok, headers: [ " Content-Disposition " : " inline; filename= \" \( name) \" " ] )
36
109
. withBody ( content, type: contentType, length: size)
37
110
}
38
111
39
112
public func download( ) async throws -> Response {
40
- Response ( status: . ok, headers: [ " Content-Disposition " : " attachment; filename= \" \( name) \" " ] )
113
+ let content = try await getContent ( )
114
+ return Response ( status: . ok, headers: [ " Content-Disposition " : " attachment; filename= \" \( name) \" " ] )
41
115
. withBody ( content, type: contentType, length: size)
42
116
}
43
117
44
- // MARK: - Decodable
45
-
46
- enum CodingKeys : String , CodingKey {
47
- case name, size, content
48
- }
118
+ // MARK: - Codable
49
119
50
120
public init ( from decoder: Decoder ) throws {
51
- let container = try decoder. container ( keyedBy: CodingKeys . self)
52
- self . name = try container. decode ( String . self, forKey: . name)
53
- self . size = try container. decode ( Int . self, forKey: . size)
54
- self . content = . data( try container. decode ( Data . self, forKey: . content) )
55
- let _extension = name. components ( separatedBy: " . " ) . last ?? " "
56
- self . contentType = ContentType ( fileExtension: _extension) ?? . octetStream
121
+ let container = try decoder. singleValueContainer ( )
122
+ let data = try container. decode ( Data . self)
123
+ self . name = UUID ( ) . uuidString
124
+ self . source = . raw
125
+ self . content = . data( data)
126
+ self . size = data. count
127
+ self . clientContentType = nil
57
128
}
58
129
59
- // MARK: - Encodable
60
-
61
130
public func encode( to encoder: Encoder ) throws {
62
- var container = encoder. container ( keyedBy: CodingKeys . self)
63
- try container. encode ( name, forKey: . name)
64
- try container. encode ( size, forKey: . size)
65
- try container. encode ( content. data ( ) , forKey: . content)
131
+ var container = encoder. singleValueContainer ( )
132
+ guard let content = content else {
133
+ throw FileError . contentNotLoaded
134
+ }
135
+
136
+ try container. encode ( content. data ( ) )
66
137
}
67
138
}
68
139
69
140
// As of now, streamed files aren't possible over request multipart.
70
141
extension File : MultipartPartConvertible {
71
142
public var multipart : MultipartPart ? {
72
143
var headers : HTTPHeaders = [ : ]
73
- headers. contentType = ContentType ( fileExtension : `extension` )
144
+ headers. contentType = contentType
74
145
headers. contentDisposition = HTTPHeaders . ContentDisposition ( value: " form-data " , name: nil , filename: name)
75
146
headers. contentLength = size
76
- return MultipartPart ( headers: headers, body: content. buffer)
147
+ guard let content = self . content else {
148
+ Log . warning ( " Unable to convert a filesystem reference to a `MultipartPart`. Please load the contents of the file first. " )
149
+ return nil
150
+ }
151
+
152
+ return MultipartPart ( headers: headers, body: content. data ( ) )
77
153
}
78
154
79
155
public init ? ( multipart: MultipartPart ) {
@@ -86,6 +162,8 @@ extension File: MultipartPartConvertible {
86
162
}
87
163
88
164
// If there is no filename in the content disposition included (technically not required via RFC 7578) set to a random UUID.
89
- self . init ( name: ( fileName ?? UUID ( ) . uuidString) + fileExtension, contentType: multipart. headers. contentType, size: fileSize, content: . buffer( multipart. body) )
165
+ let name = ( fileName ?? UUID ( ) . uuidString) + fileExtension
166
+ let contentType = multipart. headers. contentType
167
+ self . init ( name: name, source: . http( clientContentType: contentType) , content: . buffer( multipart. body) , size: fileSize)
90
168
}
91
169
}
0 commit comments