Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Move video file to purgeable storage once uploaded #42

Merged
merged 1 commit into from
May 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ORBIT Camera.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
4BE50EF72438879E00BE79C7 /* ThingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE50EF62438879E00BE79C7 /* ThingCell.swift */; };
4BE77689247910470088562D /* Video+ServerStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE77688247910470088562D /* Video+ServerStatus.swift */; };
4BE7768C247A98360088562D /* Participant+ServerStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE7768B247A98350088562D /* Participant+ServerStatus.swift */; };
4BE7768F247ABEB40088562D /* orbit-cup-photoreal.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 4BE7768A247935D30088562D /* orbit-cup-photoreal.mp4 */; };
4BF14BBA2405730700E31D93 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF14BB92405730700E31D93 /* SceneDelegate.swift */; };
4BF14BBC2405730700E31D93 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF14BBB2405730700E31D93 /* MasterViewController.swift */; };
4BF14BBE2405730700E31D93 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF14BBD2405730700E31D93 /* DetailViewController.swift */; };
Expand Down Expand Up @@ -117,6 +118,7 @@
4BE50EF42437F07C00BE79C7 /* Collection+SafeSubscript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+SafeSubscript.swift"; sourceTree = "<group>"; };
4BE50EF62438879E00BE79C7 /* ThingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThingCell.swift; sourceTree = "<group>"; };
4BE77688247910470088562D /* Video+ServerStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Video+ServerStatus.swift"; sourceTree = "<group>"; };
4BE7768A247935D30088562D /* orbit-cup-photoreal.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = "orbit-cup-photoreal.mp4"; sourceTree = "<group>"; };
4BE7768B247A98350088562D /* Participant+ServerStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Participant+ServerStatus.swift"; sourceTree = "<group>"; };
4BF14BB42405730700E31D93 /* ORBIT Camera.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ORBIT Camera.app"; sourceTree = BUILT_PRODUCTS_DIR; };
4BF14BB72405730700E31D93 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -234,6 +236,7 @@
4BFC5F222458B12800E5A3C1 /* InformedConsent.markdown */,
4BFC5F28245B6BD600E5A3C1 /* Introduction.markdown */,
4B3119AC2450977100E28452 /* Recording.markdown */,
4BE7768A247935D30088562D /* orbit-cup-photoreal.mp4 */,
4BF14BC22405730900E31D93 /* Assets.xcassets */,
4BF14BC42405730900E31D93 /* LaunchScreen.storyboard */,
4BF14BC72405730900E31D93 /* Info.plist */,
Expand Down Expand Up @@ -350,6 +353,7 @@
4B3119AE2450977100E28452 /* Recording.markdown in Resources */,
4BF14BC32405730900E31D93 /* Assets.xcassets in Resources */,
4BF14BC12405730700E31D93 /* Main.storyboard in Resources */,
4BE7768F247ABEB40088562D /* orbit-cup-photoreal.mp4 in Resources */,
4BFC5F212458ACE200E5A3C1 /* ParticipantInformation.markdown in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
24 changes: 5 additions & 19 deletions ORBIT Camera/DetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -596,27 +596,20 @@ class DetailViewController: UIViewController {
os_log("Could not get video for re-record recordStart", log: appUILog)
return
}
do {
try FileManager.default.removeItem(at: video.url)
} catch {
os_log("Could not delete previous recording to re-record", log: appUILog)
return
}

// Go, configuring completion handler that updates the UI
let url = Video.mintRecordURL()
let videoPageIndex = pageIndex
camera.recordStart(to: video.url) { [weak self] in
camera.recordStart(to: url) { [weak self] in
guard let self = self
else { return }

// Update controller state
self.rerecordPageIndexes.remove(videoPageIndex)

// Delete server record
video.deleteUpload()
video.orbitID = nil

// Update record
video.rerecordReset()
video.url = url
video.recorded = Date()
try! dbQueue.write { db in try video.save(db) }

Expand All @@ -628,16 +621,9 @@ class DetailViewController: UIViewController {
os_log("No thing on recordStart", log: appUILog)
return
}
guard let url = try? FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(NSUUID().uuidString)
.appendingPathExtension("mov")
else {
os_log("Could not create URL for recordStart", log: appUILog)
return
}

// Go, setting completion handler that creates a Video record and updates the UI
let url = Video.mintRecordURL()
let kind = videoKind(description: videoPageControl.currentCategoryName!)
camera.recordStart(to: url) {
// Create a Video record
Expand Down
89 changes: 70 additions & 19 deletions ORBIT Camera/Video.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,38 @@ struct Video: Codable, Equatable {
var thingID: Int64?

/// On-device file URL of a video the participant has recorded
/// As the app's data folder is named dynamically, only store the filename and get/set the URL relative to a type set storage location
/// To minimise iCloud backup size and low-storage scenarios, the video file will be moved to the cache folder when upload is complete.
/// If the system then culls the video file from the cache folder, a bundled-with-the-app placeholder video URL will be returned instead.
/// Plus, as the app's data folder is named dynamically, only store the filename and get/set the URL relative to a type set storage location
var url: URL {
get { URL(fileURLWithPath: filename, relativeTo: Video.storageURL) }
set { filename = newValue.lastPathComponent }
get {
var url = URL(fileURLWithPath: filename, relativeTo: Video.storageURL)
do {
try _ = url.checkResourceIsReachable()
return url
}
catch {}
url = URL(fileURLWithPath: filename, relativeTo: Video.storageCacheURL)
do {
try _ = url.checkResourceIsReachable()
return url
}
catch {}
return Video.placeholderURL
}
set {
filename = newValue.lastPathComponent
}
}

/// When the video was recorded
var recorded: Date

/// A unique ID for the thing in the ORBIT dataset (or rather, the database the dataset will be produced from)
// Note this was handled more elegantly by orbitID being an UploadStatus enum, but the supporting code was getting ridiculous.
var orbitID: Int?
var orbitID: Int? {
didSet { if oldValue == nil && orbitID != nil { moveToCacheStorage() } }
}

/// The kind of video this is.
/// Current terminology: videos are taken with one of two goals: "train" or "test", with two "techniques" used for test videos: "zoom" and "pan".
Expand Down Expand Up @@ -96,33 +116,57 @@ struct Video: Codable, Equatable {
self.url = url
}

// Private property backing `url`
private var filename: String

// Private type property backing `url`
private static var storageURL: URL {
try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) // FIXME: try!
}

// Private type property backing `url`
private static var storageCacheURL: URL {
try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) // FIXME: try!
/// Reset in preparation for a new video URL to be set. Deletes the file, deletes the server record, resets statuses pertaining to previous video
mutating func rerecordReset() {
// Cancel any in-progress upload
cancelUploading()

// Delete video from server
deleteUpload()

// Remove file
for url in [URL(fileURLWithPath: filename, relativeTo: Video.storageURL), URL(fileURLWithPath: filename, relativeTo: Video.storageURL)] {
do {
try FileManager.default.removeItem(at: url)
break
} catch {}
}

// Reset statuses
verified = .unvalidated
orbitID = nil
}

private static var placeholderURL: URL {
Bundle.main.url(forResource: "orbit-cup-photoreal", withExtension: "mp4")! // FIXME: !
/// Generate a URL suitable for recording a video and then setting this URL as the video's property
static func mintRecordURL() -> URL {
Video.storageURL
.appendingPathComponent(NSUUID().uuidString)
.appendingPathExtension("mov")
}

// Private property backing `url`
private var filename: String

// Private method backing `url`
private func moveToCacheStorage() {
do {
try FileManager.default.moveItem(
at: URL(fileURLWithPath: filename, relativeTo: Video.storageURL),
to: URL(fileURLWithPath: filename, relativeTo: Video.storageCacheURL)
)
} catch {
print(error)
os_log("Move of %{public}s file to cache failed", self.description)
}
}

// Private type property backing `url`
private static let storageURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) // FIXME: try!

// Private type property backing `url`
private static let storageCacheURL = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) // FIXME: try!

// Private type property backing `url`
private static let placeholderURL = Bundle.main.url(forResource: "orbit-cup-photoreal", withExtension: "mp4")! // FIXME: !
}

extension Video: FetchableRecord, MutablePersistableRecord {
Expand Down Expand Up @@ -152,7 +196,14 @@ extension Video: FetchableRecord, MutablePersistableRecord {
deleteUpload()

// Remove file
try FileManager.default.removeItem(at: url)
for url in [URL(fileURLWithPath: filename, relativeTo: Video.storageURL), URL(fileURLWithPath: filename, relativeTo: Video.storageURL)] {
do {
try FileManager.default.removeItem(at: url)
break
} catch {}
}

// Delete record
let deleted = try performDelete(db)
if !deleted { os_log("Failed to delete Video") }
return deleted
Expand Down
Binary file added ORBIT Camera/orbit-cup-photoreal.mp4
Binary file not shown.