From 7fcdf1481f612be235659656d1864d1a1c787a7e Mon Sep 17 00:00:00 2001 From: Pim Coumans Date: Mon, 29 Oct 2018 14:51:56 +0100 Subject: [PATCH 1/5] Set tabs preferences for all Swift files --- WeTransfer.xcodeproj/project.pbxproj | 100 +++++++++++++-------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/WeTransfer.xcodeproj/project.pbxproj b/WeTransfer.xcodeproj/project.pbxproj index a5781e1..f7b0ee2 100644 --- a/WeTransfer.xcodeproj/project.pbxproj +++ b/WeTransfer.xcodeproj/project.pbxproj @@ -151,68 +151,68 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 3E0EEBA720BEE918009E6B2D /* AsynchronousDependencyResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsynchronousDependencyResultOperation.swift; sourceTree = ""; }; - 3E0EEBAF20C1444C009E6B2D /* ChainedAsynchronousResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainedAsynchronousResultOperation.swift; sourceTree = ""; }; - 3E0EEBB120C17264009E6B2D /* CreateTransferOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateTransferOperation.swift; sourceTree = ""; }; - 3E0EEBB320C175D0009E6B2D /* AddFilesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFilesOperation.swift; sourceTree = ""; }; - 3E0EEBB520C177FE009E6B2D /* CreateChunkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateChunkOperation.swift; sourceTree = ""; }; - 3E0EEBB920C18FA5009E6B2D /* UploadFilesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadFilesOperation.swift; sourceTree = ""; }; - 3E0EEBBB20C19004009E6B2D /* UploadChunkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadChunkOperation.swift; sourceTree = ""; }; - 3E0EEBBD20C19037009E6B2D /* CompleteUploadOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteUploadOperation.swift; sourceTree = ""; }; - 3E0EEBBF20C19074009E6B2D /* UploadFileOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadFileOperation.swift; sourceTree = ""; }; - 3E3A87622162798800568775 /* CreateBoardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBoardTests.swift; sourceTree = ""; }; + 3E0EEBA720BEE918009E6B2D /* AsynchronousDependencyResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsynchronousDependencyResultOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3E0EEBAF20C1444C009E6B2D /* ChainedAsynchronousResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainedAsynchronousResultOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3E0EEBB120C17264009E6B2D /* CreateTransferOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateTransferOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3E0EEBB320C175D0009E6B2D /* AddFilesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFilesOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3E0EEBB520C177FE009E6B2D /* CreateChunkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateChunkOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3E0EEBB920C18FA5009E6B2D /* UploadFilesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadFilesOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3E0EEBBB20C19004009E6B2D /* UploadChunkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadChunkOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3E0EEBBD20C19037009E6B2D /* CompleteUploadOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteUploadOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3E0EEBBF20C19074009E6B2D /* UploadFileOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadFileOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3E3A87622162798800568775 /* CreateBoardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBoardTests.swift; sourceTree = ""; usesTabs = 0; }; 3E3B9D2720F60F7B0064915C /* smallImage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = smallImage.jpg; sourceTree = ""; }; - 3E3B9D3B20F7ADDC0064915C /* MediaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPicker.swift; sourceTree = ""; }; + 3E3B9D3B20F7ADDC0064915C /* MediaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPicker.swift; sourceTree = ""; usesTabs = 0; }; 3E579B202092194F008DFFD2 /* image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = image.jpg; sourceTree = ""; }; 3E5E513420EA813D00485FA3 /* WeTransfer Sample Project.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WeTransfer Sample Project.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3E5E514720EA816600485FA3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; + 3E5E514720EA816600485FA3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; usesTabs = 0; }; 3E5E514820EA816600485FA3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 3E5E514A20EA816600485FA3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 3E5E514C20EA816600485FA3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 3E5E514D20EA816600485FA3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 3E5E514D20EA816600485FA3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; usesTabs = 0; }; 3E7116DC20FC85D400115E55 /* WeTransfer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WeTransfer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3E7116E420FC85D400115E55 /* WeTransfer iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "WeTransfer iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 3E7116F820FC85F100115E55 /* WeTransfer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WeTransfer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3E71170020FC85F100115E55 /* WeTransfer macOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "WeTransfer macOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3E7219A82174C7FC00143492 /* BaseTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTestCase.swift; sourceTree = ""; }; - 3E7348BD20B4040D009B41D0 /* TestConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConfiguration.swift; sourceTree = ""; }; - 3E7348BF20B41895009B41D0 /* InitializationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitializationTests.swift; sourceTree = ""; }; - 3E7348C120B41936009B41D0 /* SimpleTransferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleTransferTests.swift; sourceTree = ""; }; - 3E7348C320B41DBA009B41D0 /* AuthorizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationTests.swift; sourceTree = ""; }; - 3E7348C520B41EC0009B41D0 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = ""; usesTabs = 1; }; - 3E80E0A62091D0E700114711 /* WeTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeTransfer.swift; sourceTree = ""; }; - 3E80E0A92091D12B00114711 /* Transfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transfer.swift; sourceTree = ""; }; - 3E80E0AD2091D16E00114711 /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; - 3E8EB08B20E503E2002EB5AB /* Authenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; - 3E90734620BBEF3700AE3605 /* TransferChunksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferChunksTests.swift; sourceTree = ""; }; - 3E90734820BBFE5600AE3605 /* TransferUploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferUploadTests.swift; sourceTree = ""; }; - 3E9413BC2098BC25003A42D1 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; - 3E9413BE2098BF5A003A42D1 /* Endpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoints.swift; sourceTree = ""; }; - 3E9413C12098C132003A42D1 /* Authorize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authorize.swift; sourceTree = ""; }; - 3E9413C32099D78A003A42D1 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; - 3E9413C52099D823003A42D1 /* CreateTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateTransfer.swift; sourceTree = ""; }; - 3E9413C72099D89E003A42D1 /* AddFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFiles.swift; sourceTree = ""; }; - 3E9413C92099DB05003A42D1 /* Upload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Upload.swift; sourceTree = ""; }; - 3E9413CB2099E390003A42D1 /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; - 3E9413CD2099E3F5003A42D1 /* Chunk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chunk.swift; sourceTree = ""; }; - 3E9D48CF21663EE400499760 /* Transferable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transferable.swift; sourceTree = ""; }; - 3E9D48D22166535D00499760 /* SimpleBoardUploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleBoardUploadTests.swift; sourceTree = ""; }; - 3E9D48D52166537C00499760 /* BoardUploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardUploadTests.swift; sourceTree = ""; }; - 3E9D48DB216B6FBC00499760 /* FinalizeTransferOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinalizeTransferOperation.swift; sourceTree = ""; }; - 3E9D48E0216B89CB00499760 /* BoardChunksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardChunksTests.swift; sourceTree = ""; }; - 3EA0EFB920BD4D90009E4BB1 /* AsynchronousOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsynchronousOperation.swift; sourceTree = ""; }; - 3EA0EFCD20BD7743009E4BB1 /* AsynchronousResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsynchronousResultOperation.swift; sourceTree = ""; }; - 3EB1E191215916AF00E1E4EE /* Board.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Board.swift; sourceTree = ""; }; - 3EB1E1972159283600E1E4EE /* CreateBoard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBoard.swift; sourceTree = ""; }; - 3EB1E19A215928BF00E1E4EE /* CreateBoardOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBoardOperation.swift; sourceTree = ""; }; - 3ED1C0A220B43E980045D8B3 /* CreateTransferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateTransferTests.swift; sourceTree = ""; }; - 3ED1C0A420B442F70045D8B3 /* AddFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFilesTests.swift; sourceTree = ""; }; - 3ED6ED3920EA2AD100130261 /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = ""; }; - 3ED6ED3F20EA505800130261 /* APIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIEndpoint.swift; sourceTree = ""; }; - 3EE259662091CF8800DC5A97 /* WeTransfer_Swift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WeTransfer_Swift.h; sourceTree = ""; }; + 3E7219A82174C7FC00143492 /* BaseTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTestCase.swift; sourceTree = ""; usesTabs = 0; }; + 3E7348BD20B4040D009B41D0 /* TestConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConfiguration.swift; sourceTree = ""; usesTabs = 0; }; + 3E7348BF20B41895009B41D0 /* InitializationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitializationTests.swift; sourceTree = ""; usesTabs = 0; }; + 3E7348C120B41936009B41D0 /* SimpleTransferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleTransferTests.swift; sourceTree = ""; usesTabs = 0; }; + 3E7348C320B41DBA009B41D0 /* AuthorizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationTests.swift; sourceTree = ""; usesTabs = 0; }; + 3E7348C520B41EC0009B41D0 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = ""; usesTabs = 0; }; + 3E80E0A62091D0E700114711 /* WeTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeTransfer.swift; sourceTree = ""; usesTabs = 0; }; + 3E80E0A92091D12B00114711 /* Transfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transfer.swift; sourceTree = ""; usesTabs = 0; }; + 3E80E0AD2091D16E00114711 /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; usesTabs = 0; }; + 3E8EB08B20E503E2002EB5AB /* Authenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; usesTabs = 0; }; + 3E90734620BBEF3700AE3605 /* TransferChunksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferChunksTests.swift; sourceTree = ""; usesTabs = 0; }; + 3E90734820BBFE5600AE3605 /* TransferUploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferUploadTests.swift; sourceTree = ""; usesTabs = 0; }; + 3E9413BC2098BC25003A42D1 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; usesTabs = 0; }; + 3E9413BE2098BF5A003A42D1 /* Endpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoints.swift; sourceTree = ""; usesTabs = 0; }; + 3E9413C12098C132003A42D1 /* Authorize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authorize.swift; sourceTree = ""; usesTabs = 0; }; + 3E9413C32099D78A003A42D1 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; usesTabs = 0; }; + 3E9413C52099D823003A42D1 /* CreateTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateTransfer.swift; sourceTree = ""; usesTabs = 0; }; + 3E9413C72099D89E003A42D1 /* AddFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFiles.swift; sourceTree = ""; usesTabs = 0; }; + 3E9413C92099DB05003A42D1 /* Upload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Upload.swift; sourceTree = ""; usesTabs = 0; }; + 3E9413CB2099E390003A42D1 /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; usesTabs = 0; }; + 3E9413CD2099E3F5003A42D1 /* Chunk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chunk.swift; sourceTree = ""; usesTabs = 0; }; + 3E9D48CF21663EE400499760 /* Transferable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transferable.swift; sourceTree = ""; usesTabs = 0; }; + 3E9D48D22166535D00499760 /* SimpleBoardUploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleBoardUploadTests.swift; sourceTree = ""; usesTabs = 0; }; + 3E9D48D52166537C00499760 /* BoardUploadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardUploadTests.swift; sourceTree = ""; usesTabs = 0; }; + 3E9D48DB216B6FBC00499760 /* FinalizeTransferOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinalizeTransferOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3E9D48E0216B89CB00499760 /* BoardChunksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardChunksTests.swift; sourceTree = ""; usesTabs = 0; }; + 3EA0EFB920BD4D90009E4BB1 /* AsynchronousOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsynchronousOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3EA0EFCD20BD7743009E4BB1 /* AsynchronousResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsynchronousResultOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3EB1E191215916AF00E1E4EE /* Board.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Board.swift; sourceTree = ""; usesTabs = 0; }; + 3EB1E1972159283600E1E4EE /* CreateBoard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBoard.swift; sourceTree = ""; usesTabs = 0; }; + 3EB1E19A215928BF00E1E4EE /* CreateBoardOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBoardOperation.swift; sourceTree = ""; usesTabs = 0; }; + 3ED1C0A220B43E980045D8B3 /* CreateTransferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateTransferTests.swift; sourceTree = ""; usesTabs = 0; }; + 3ED1C0A420B442F70045D8B3 /* AddFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFilesTests.swift; sourceTree = ""; usesTabs = 0; }; + 3ED6ED3920EA2AD100130261 /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = ""; usesTabs = 0; }; + 3ED6ED3F20EA505800130261 /* APIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIEndpoint.swift; sourceTree = ""; usesTabs = 0; }; + 3EE259662091CF8800DC5A97 /* WeTransfer_Swift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WeTransfer_Swift.h; sourceTree = ""; usesTabs = 0; }; 3EE259672091CF8800DC5A97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3EE259732091CF8800DC5A97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 3EFDC97F21012FD70091CF85 /* RoundedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedButton.swift; sourceTree = ""; }; + 3EFDC97F21012FD70091CF85 /* RoundedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedButton.swift; sourceTree = ""; usesTabs = 0; }; 3EFDC9B4210891220091CF85 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ From 12b046409d10ddd8ecbf23edab78d1f6875ba533 Mon Sep 17 00:00:00 2001 From: Pim Coumans Date: Mon, 29 Oct 2018 15:05:55 +0100 Subject: [PATCH 2/5] Re-indented all swift files with 4 spaces --- .../MainViewController.swift | 410 ++++++++--------- WeTransfer Sample Project/MediaPicker.swift | 194 ++++---- WeTransfer Sample Project/RoundedButton.swift | 102 ++--- .../Supporting Files/AppDelegate.swift | 12 +- WeTransfer/Models/Board.swift | 74 ++-- WeTransfer/Models/Chunk.swift | 82 ++-- WeTransfer/Models/File.swift | 112 ++--- WeTransfer/Models/Transfer.swift | 50 +-- WeTransfer/Models/Transferable.swift | 6 +- WeTransfer/Server/APIClient.swift | 120 ++--- WeTransfer/Server/Authenticator.swift | 56 +-- WeTransfer/Server/Endpoints/APIEndpoint.swift | 64 +-- WeTransfer/Server/Endpoints/Endpoints.swift | 418 +++++++++--------- WeTransfer/Server/Methods/AddFiles.swift | 60 +-- WeTransfer/Server/Methods/Authorize.swift | 72 +-- WeTransfer/Server/Methods/CreateBoard.swift | 40 +- .../Server/Methods/CreateTransfer.swift | 46 +- WeTransfer/Server/Methods/Request.swift | 248 +++++------ WeTransfer/Server/Methods/Upload.swift | 140 +++--- ...synchronousDependencyResultOperation.swift | 34 +- .../Abstract/AsynchronousOperation.swift | 298 ++++++------- .../AsynchronousResultOperation.swift | 64 +-- .../ChainedAsynchronousResultOperation.swift | 192 ++++---- .../Server/Operations/AddFilesOperation.swift | 132 +++--- .../Operations/CompleteUploadOperation.swift | 178 ++++---- .../Operations/CreateBoardOperation.swift | 80 ++-- .../Operations/CreateChunkOperation.swift | 130 +++--- .../Operations/CreateTransferOperation.swift | 134 +++--- .../FinalizeTransferOperation.swift | 60 +-- .../Server/Operations/Helpers/Result.swift | 28 +- .../Operations/UploadChunkOperation.swift | 98 ++-- .../Operations/UploadFileOperation.swift | 142 +++--- .../Operations/UploadFilesOperation.swift | 178 ++++---- WeTransfer/WeTransfer.swift | 352 +++++++-------- WeTransferTests/AuthorizationTests.swift | 70 +-- WeTransferTests/BaseTestCase.swift | 14 +- WeTransferTests/Board/AddFilesTests.swift | 218 ++++----- WeTransferTests/Board/BoardChunksTests.swift | 102 ++--- WeTransferTests/Board/BoardUploadTests.swift | 106 ++--- WeTransferTests/Board/CreateBoardTests.swift | 28 +- .../Board/SimpleBoardUploadTests.swift | 80 ++-- WeTransferTests/InitializationTests.swift | 38 +- WeTransferTests/RequestTests.swift | 148 +++---- WeTransferTests/Secrets.swift | 78 ++-- WeTransferTests/TestConfiguration.swift | 98 ++-- .../Transfer/CreateTransferTests.swift | 62 +-- .../Transfer/SimpleTransferTests.swift | 84 ++-- .../Transfer/TransferChunksTests.swift | 104 ++--- .../Transfer/TransferUploadTests.swift | 144 +++--- 49 files changed, 2890 insertions(+), 2890 deletions(-) diff --git a/WeTransfer Sample Project/MainViewController.swift b/WeTransfer Sample Project/MainViewController.swift index de79592..be447a5 100644 --- a/WeTransfer Sample Project/MainViewController.swift +++ b/WeTransfer Sample Project/MainViewController.swift @@ -13,221 +13,221 @@ import WeTransfer /// Actual logic for configuring the WeTransfer client and performing the transfer is found in the first extension marked 'WeTransfer Logic' /// To properly authenticate with the client make sure you've created an API key at https://developers.wetransfer.com final class MainViewController: UIViewController { - - /// Used to decide which views should be shown and what the content of the labels should be - private enum ViewState { - case ready - case selectedMedia - case startedTransfer - case transferInProgress - case failed(error: Error) - case transferCompleted(shortURL: URL) - } - - @IBOutlet private var titleLabel: UILabel! - @IBOutlet private var bodyLabel: UILabel! - @IBOutlet private var selectButton: UIButton! - @IBOutlet private var progressView: UIProgressView! - @IBOutlet private var urlButton: UIButton! - - @IBOutlet private var imageView: UIImageView! - @IBOutlet private var secondImageView: UIImageView! - - @IBOutlet private var transferButton: UIButton! - @IBOutlet private var addMoreButton: RoundedButton! - @IBOutlet private var shareButton: UIButton! - @IBOutlet private var newTransferButton: RoundedButton! - - @IBOutlet private var mainButtonsStackView: UIStackView! - @IBOutlet private var contentStackView: UIStackView! - - /// Handles presentation of UIImagePickerController - let picker = MediaPicker() - - private var viewState: ViewState = .ready { - didSet { - updateInterface() - } - } - - private var progressObservation: NSKeyValueObservation? - - private var selectedMedia = [MediaPicker.Media]() { - didSet { - // Update image views whenever media is selected - imageView.image = selectedMedia.last?.previewImage - if selectedMedia.count > 1 { - let image = selectedMedia[selectedMedia.count - 2].previewImage - secondImageView.image = image - } else { - secondImageView.image = nil - } - } - } - - /// Holds completed transfer's URL - private var transferURL: URL? - - override func viewDidLoad() { - super.viewDidLoad() - newTransferButton.style = .alternative - addMoreButton.style = .alternative - - rotateSecondImage() - updateInterface() - configureWeTransfer() - } + + /// Used to decide which views should be shown and what the content of the labels should be + private enum ViewState { + case ready + case selectedMedia + case startedTransfer + case transferInProgress + case failed(error: Error) + case transferCompleted(shortURL: URL) + } + + @IBOutlet private var titleLabel: UILabel! + @IBOutlet private var bodyLabel: UILabel! + @IBOutlet private var selectButton: UIButton! + @IBOutlet private var progressView: UIProgressView! + @IBOutlet private var urlButton: UIButton! + + @IBOutlet private var imageView: UIImageView! + @IBOutlet private var secondImageView: UIImageView! + + @IBOutlet private var transferButton: UIButton! + @IBOutlet private var addMoreButton: RoundedButton! + @IBOutlet private var shareButton: UIButton! + @IBOutlet private var newTransferButton: RoundedButton! + + @IBOutlet private var mainButtonsStackView: UIStackView! + @IBOutlet private var contentStackView: UIStackView! + + /// Handles presentation of UIImagePickerController + let picker = MediaPicker() + + private var viewState: ViewState = .ready { + didSet { + updateInterface() + } + } + + private var progressObservation: NSKeyValueObservation? + + private var selectedMedia = [MediaPicker.Media]() { + didSet { + // Update image views whenever media is selected + imageView.image = selectedMedia.last?.previewImage + if selectedMedia.count > 1 { + let image = selectedMedia[selectedMedia.count - 2].previewImage + secondImageView.image = image + } else { + secondImageView.image = nil + } + } + } + + /// Holds completed transfer's URL + private var transferURL: URL? + + override func viewDidLoad() { + super.viewDidLoad() + newTransferButton.style = .alternative + addMoreButton.style = .alternative + + rotateSecondImage() + updateInterface() + configureWeTransfer() + } } // MARK: - WeTransfer Logic extension MainViewController { - - private func configureWeTransfer() { - // Configures the WeTransfer client with the required API key - // Get an API key at https://developers.wetransfer.com - WeTransfer.configure(with: WeTransfer.Configuration(apiKey: "{YOUR_API_KEY_HERE}")) - } - - private func sendTransfer() { - guard !selectedMedia.isEmpty else { - return - } - viewState = .startedTransfer - let files = selectedMedia.map({ $0.url }) - - // Creates a transfer and uploads all provided files - WeTransfer.uploadTransfer(saying: "Sample Transfer", containing: files) { [weak self] state in - switch state { - case .uploading(let progress): - self?.viewState = .transferInProgress - self?.observeUploadProgress(progress) - case .failed(let error): - self?.viewState = .failed(error: error) - case .completed(let transfer): - if let url = transfer.shortURL { - self?.transferURL = url - self?.viewState = .transferCompleted(shortURL: url) - } - default: - break - } - } - } - - private func observeUploadProgress(_ progress: Progress) { - progressView.observedProgress = progress - progressObservation = progress.observe(\.fractionCompleted) { [weak self] (progress, _) in - DispatchQueue.main.async { - self?.bodyLabel.text = "\(Int(progress.fractionCompleted * 100))% completed" - } - } - } + + private func configureWeTransfer() { + // Configures the WeTransfer client with the required API key + // Get an API key at https://developers.wetransfer.com + WeTransfer.configure(with: WeTransfer.Configuration(apiKey: "{YOUR_API_KEY_HERE}")) + } + + private func sendTransfer() { + guard !selectedMedia.isEmpty else { + return + } + viewState = .startedTransfer + let files = selectedMedia.map({ $0.url }) + + // Creates a transfer and uploads all provided files + WeTransfer.uploadTransfer(saying: "Sample Transfer", containing: files) { [weak self] state in + switch state { + case .uploading(let progress): + self?.viewState = .transferInProgress + self?.observeUploadProgress(progress) + case .failed(let error): + self?.viewState = .failed(error: error) + case .completed(let transfer): + if let url = transfer.shortURL { + self?.transferURL = url + self?.viewState = .transferCompleted(shortURL: url) + } + default: + break + } + } + } + + private func observeUploadProgress(_ progress: Progress) { + progressView.observedProgress = progress + progressObservation = progress.observe(\.fractionCompleted) { [weak self] (progress, _) in + DispatchQueue.main.async { + self?.bodyLabel.text = "\(Int(progress.fractionCompleted * 100))% completed" + } + } + } } // MARK: - UI Logic extension MainViewController { - - private func rotateSecondImage() { - let rotation = 2 * (CGFloat.pi / 180) - secondImageView.transform = CGAffineTransform(rotationAngle: rotation) - } - - private func resetInterface() { - // Remove UI elements that aren't used everywhere - [transferButton, addMoreButton, shareButton, newTransferButton].forEach({ (button: UIButton) in - mainButtonsStackView.removeArrangedSubview(button) - button.removeFromSuperview() - }) - - // Hide views not managed by a UIStackView - imageView.isHidden = true - secondImageView.isHidden = true - - [selectButton, progressView, urlButton].forEach({ (element: UIView) in - contentStackView.removeArrangedSubview(element) - element.removeFromSuperview() - }) - } - - private func updateInterface() { - resetInterface() - - switch viewState { - case .ready: - transferButton.setTitle("Transfer", for: .normal) - mainButtonsStackView.addArrangedSubview(transferButton) - transferButton.isEnabled = false - titleLabel.text = "Add media to transfer" - bodyLabel.text = "Pick a photo to send and get a URL to share wherever you want" - contentStackView.addArrangedSubview(selectButton) - case .selectedMedia: - titleLabel.text = nil - bodyLabel.text = nil - imageView.isHidden = false - secondImageView.isHidden = false - mainButtonsStackView.addArrangedSubview(addMoreButton) - mainButtonsStackView.addArrangedSubview(transferButton) - transferButton.isEnabled = true - case .startedTransfer: - titleLabel.text = "Uploading" - bodyLabel.text = "Preparing transfer..." - progressView.progress = 0 - contentStackView.addArrangedSubview(progressView) - case .transferInProgress: - titleLabel.text = "Uploading" - contentStackView.addArrangedSubview(progressView) - case .failed(let error): - titleLabel.text = "Upload failed" - bodyLabel.text = error.localizedDescription - transferButton.setTitle("Retry transfer", for: .normal) - mainButtonsStackView.addArrangedSubview(transferButton) - case .transferCompleted(let shortURL): - titleLabel.text = "Transfer completed" - bodyLabel.text = nil - let attributes: [NSAttributedString.Key: Any] = [.underlineStyle: NSUnderlineStyle.single.rawValue, - .foregroundColor: urlButton.currentTitleColor] - let attributedURLText = NSAttributedString(string: shortURL.absoluteString, attributes: attributes) - urlButton.setAttributedTitle(attributedURLText, for: .normal) - contentStackView.addArrangedSubview(urlButton) - mainButtonsStackView.addArrangedSubview(shareButton) - mainButtonsStackView.addArrangedSubview(newTransferButton) - } - } + + private func rotateSecondImage() { + let rotation = 2 * (CGFloat.pi / 180) + secondImageView.transform = CGAffineTransform(rotationAngle: rotation) + } + + private func resetInterface() { + // Remove UI elements that aren't used everywhere + [transferButton, addMoreButton, shareButton, newTransferButton].forEach({ (button: UIButton) in + mainButtonsStackView.removeArrangedSubview(button) + button.removeFromSuperview() + }) + + // Hide views not managed by a UIStackView + imageView.isHidden = true + secondImageView.isHidden = true + + [selectButton, progressView, urlButton].forEach({ (element: UIView) in + contentStackView.removeArrangedSubview(element) + element.removeFromSuperview() + }) + } + + private func updateInterface() { + resetInterface() + + switch viewState { + case .ready: + transferButton.setTitle("Transfer", for: .normal) + mainButtonsStackView.addArrangedSubview(transferButton) + transferButton.isEnabled = false + titleLabel.text = "Add media to transfer" + bodyLabel.text = "Pick a photo to send and get a URL to share wherever you want" + contentStackView.addArrangedSubview(selectButton) + case .selectedMedia: + titleLabel.text = nil + bodyLabel.text = nil + imageView.isHidden = false + secondImageView.isHidden = false + mainButtonsStackView.addArrangedSubview(addMoreButton) + mainButtonsStackView.addArrangedSubview(transferButton) + transferButton.isEnabled = true + case .startedTransfer: + titleLabel.text = "Uploading" + bodyLabel.text = "Preparing transfer..." + progressView.progress = 0 + contentStackView.addArrangedSubview(progressView) + case .transferInProgress: + titleLabel.text = "Uploading" + contentStackView.addArrangedSubview(progressView) + case .failed(let error): + titleLabel.text = "Upload failed" + bodyLabel.text = error.localizedDescription + transferButton.setTitle("Retry transfer", for: .normal) + mainButtonsStackView.addArrangedSubview(transferButton) + case .transferCompleted(let shortURL): + titleLabel.text = "Transfer completed" + bodyLabel.text = nil + let attributes: [NSAttributedString.Key: Any] = [.underlineStyle: NSUnderlineStyle.single.rawValue, + .foregroundColor: urlButton.currentTitleColor] + let attributedURLText = NSAttributedString(string: shortURL.absoluteString, attributes: attributes) + urlButton.setAttributedTitle(attributedURLText, for: .normal) + contentStackView.addArrangedSubview(urlButton) + mainButtonsStackView.addArrangedSubview(shareButton) + mainButtonsStackView.addArrangedSubview(newTransferButton) + } + } } // MARK: - Button handlers extension MainViewController { - - @IBAction private func didPressSelectButton(_ button: UIButton) { - picker.show(from: self) { [weak self] (media) in - if let media = media { - self?.selectedMedia.append(media) - self?.viewState = .selectedMedia - } - } - } - - @IBAction private func didPressTransferButton(_ button: UIButton) { - sendTransfer() - } - - @IBAction private func didPressShareButton(_ button: UIButton) { - guard let url = transferURL else { - return - } - let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil) - present(activityViewController, animated: true, completion: nil) - } - - @IBAction private func didPressNewTransferButton(_ button: UIButton) { - selectedMedia.removeAll() - viewState = .ready - } - - @IBAction private func didPressURLButton(_ button: UIButton) { - guard let url = transferURL, UIApplication.shared.canOpenURL(url) else { - return - } - UIApplication.shared.open(url) - } + + @IBAction private func didPressSelectButton(_ button: UIButton) { + picker.show(from: self) { [weak self] (media) in + if let media = media { + self?.selectedMedia.append(media) + self?.viewState = .selectedMedia + } + } + } + + @IBAction private func didPressTransferButton(_ button: UIButton) { + sendTransfer() + } + + @IBAction private func didPressShareButton(_ button: UIButton) { + guard let url = transferURL else { + return + } + let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil) + present(activityViewController, animated: true, completion: nil) + } + + @IBAction private func didPressNewTransferButton(_ button: UIButton) { + selectedMedia.removeAll() + viewState = .ready + } + + @IBAction private func didPressURLButton(_ button: UIButton) { + guard let url = transferURL, UIApplication.shared.canOpenURL(url) else { + return + } + UIApplication.shared.open(url) + } } diff --git a/WeTransfer Sample Project/MediaPicker.swift b/WeTransfer Sample Project/MediaPicker.swift index 40780a2..22c54c3 100644 --- a/WeTransfer Sample Project/MediaPicker.swift +++ b/WeTransfer Sample Project/MediaPicker.swift @@ -11,104 +11,104 @@ import Photos /// Basic wrapper for UIImagePickerController to handle authorization and configuring and presenting the controller final class MediaPicker: NSObject { - - struct Media { - let url: URL - let previewImage: UIImage - } - - typealias PickedMediaHandler = (_ media: Media?) -> Void - - private var mediaHandler: PickedMediaHandler? - private var presentedImagePickerControler: UIImagePickerController? - - func show(from viewController: UIViewController, mediaHandler: @escaping PickedMediaHandler) { - guard self.mediaHandler == nil else { - return - } - self.mediaHandler = mediaHandler - authorize { [weak self] (succeeded) in - guard succeeded, UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { - self?.finish(with: nil) - return - } - self?.presentImagePicker(from: viewController) - } - } - - private func presentImagePicker(from viewController: UIViewController) { - let imagePickerController = UIImagePickerController() - imagePickerController.delegate = self - imagePickerController.sourceType = .photoLibrary - if let mediaTypes = UIImagePickerController.availableMediaTypes(for: .photoLibrary) { - imagePickerController.mediaTypes = mediaTypes - } - viewController.present(imagePickerController, animated: true, completion: nil) - presentedImagePickerControler = imagePickerController - } - - private func authorize(with completion: @escaping (Bool) -> Void) { - switch PHPhotoLibrary.authorizationStatus() { - case .authorized: - completion(true) - case .denied, .restricted: - completion(false) - case .notDetermined: - PHPhotoLibrary.requestAuthorization { (status) in - guard status == .authorized else { - completion(false) - return - } - completion(true) - } - } - } - - private func finish(with item: URL?) { - guard let url = item else { - mediaHandler?(nil) - dismissPickerController() - return - } - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - var pickedMedia: Media? - let asset = AVAsset(url: url) - if asset.duration.seconds > 0 { - // Get first frame if video - let imageGenerator = AVAssetImageGenerator(asset: asset) - imageGenerator.appliesPreferredTrackTransform = true - if let image = try? imageGenerator.copyCGImage(at: CMTime.zero, actualTime: nil) { - pickedMedia = Media(url: url, previewImage: UIImage(cgImage: image)) - } - } else { - if let image = UIImage(contentsOfFile: url.path) { - pickedMedia = Media(url: url, previewImage: image) - } - } - DispatchQueue.main.async { - self?.mediaHandler?(pickedMedia) - self?.dismissPickerController() - } - } - } - - private func dismissPickerController() { - mediaHandler = nil - presentedImagePickerControler?.presentingViewController?.dismiss(animated: true, completion: nil) - presentedImagePickerControler = nil - } + + struct Media { + let url: URL + let previewImage: UIImage + } + + typealias PickedMediaHandler = (_ media: Media?) -> Void + + private var mediaHandler: PickedMediaHandler? + private var presentedImagePickerControler: UIImagePickerController? + + func show(from viewController: UIViewController, mediaHandler: @escaping PickedMediaHandler) { + guard self.mediaHandler == nil else { + return + } + self.mediaHandler = mediaHandler + authorize { [weak self] (succeeded) in + guard succeeded, UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { + self?.finish(with: nil) + return + } + self?.presentImagePicker(from: viewController) + } + } + + private func presentImagePicker(from viewController: UIViewController) { + let imagePickerController = UIImagePickerController() + imagePickerController.delegate = self + imagePickerController.sourceType = .photoLibrary + if let mediaTypes = UIImagePickerController.availableMediaTypes(for: .photoLibrary) { + imagePickerController.mediaTypes = mediaTypes + } + viewController.present(imagePickerController, animated: true, completion: nil) + presentedImagePickerControler = imagePickerController + } + + private func authorize(with completion: @escaping (Bool) -> Void) { + switch PHPhotoLibrary.authorizationStatus() { + case .authorized: + completion(true) + case .denied, .restricted: + completion(false) + case .notDetermined: + PHPhotoLibrary.requestAuthorization { (status) in + guard status == .authorized else { + completion(false) + return + } + completion(true) + } + } + } + + private func finish(with item: URL?) { + guard let url = item else { + mediaHandler?(nil) + dismissPickerController() + return + } + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + var pickedMedia: Media? + let asset = AVAsset(url: url) + if asset.duration.seconds > 0 { + // Get first frame if video + let imageGenerator = AVAssetImageGenerator(asset: asset) + imageGenerator.appliesPreferredTrackTransform = true + if let image = try? imageGenerator.copyCGImage(at: CMTime.zero, actualTime: nil) { + pickedMedia = Media(url: url, previewImage: UIImage(cgImage: image)) + } + } else { + if let image = UIImage(contentsOfFile: url.path) { + pickedMedia = Media(url: url, previewImage: image) + } + } + DispatchQueue.main.async { + self?.mediaHandler?(pickedMedia) + self?.dismissPickerController() + } + } + } + + private func dismissPickerController() { + mediaHandler = nil + presentedImagePickerControler?.presentingViewController?.dismiss(animated: true, completion: nil) + presentedImagePickerControler = nil + } } extension MediaPicker: UIImagePickerControllerDelegate, UINavigationControllerDelegate { - func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - finish(with: nil) - } - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - guard let url = info[UIImagePickerController.InfoKey.imageURL] as? URL ?? info[UIImagePickerController.InfoKey.mediaURL] as? URL else { - finish(with: nil) - return - } - finish(with: url) - } + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + finish(with: nil) + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + guard let url = info[UIImagePickerController.InfoKey.imageURL] as? URL ?? info[UIImagePickerController.InfoKey.mediaURL] as? URL else { + finish(with: nil) + return + } + finish(with: url) + } } diff --git a/WeTransfer Sample Project/RoundedButton.swift b/WeTransfer Sample Project/RoundedButton.swift index c1e6a64..0c05b96 100644 --- a/WeTransfer Sample Project/RoundedButton.swift +++ b/WeTransfer Sample Project/RoundedButton.swift @@ -10,55 +10,55 @@ import UIKit /// Button with colored background and rounded corners final class RoundedButton: UIButton { - - enum Style { - case regular - case alternative - } - - private let horizontalPadding: CGFloat = 22 - private let minimumHeight: CGFloat = 44 - - var style: Style = .regular { - didSet { - updateBackgroundColor() - updateTitleColor() - } - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - layer.cornerRadius = 8 - updateBackgroundColor() - updateTitleColor() - titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .medium) - - heightAnchor.constraint(greaterThanOrEqualToConstant: minimumHeight).isActive = true - contentEdgeInsets = UIEdgeInsets(top: 0, left: horizontalPadding, bottom: 0, right: horizontalPadding) - } - - override var isEnabled: Bool { - didSet { - updateBackgroundColor() - updateTitleColor() - } - } - - private func updateBackgroundColor() { - if isEnabled { - let enabledColor = UIColor(red: 64 / 255, green: 159 / 255, blue: 255 / 255, alpha: 1) - let alternativeColor = UIColor(white: 235 / 255, alpha: 1) - backgroundColor = style == .alternative ? alternativeColor : enabledColor - } else { - backgroundColor = UIColor(red: 165 / 255, green: 168 / 255, blue: 172 / 255, alpha: 1) - } - - } - - private func updateTitleColor() { - let titleColor = UIColor.white - let alternativeTitleColor = UIColor(white: 75 / 255, alpha: 1) - setTitleColor(style == .alternative ? alternativeTitleColor : titleColor, for: .normal) - } - + + enum Style { + case regular + case alternative + } + + private let horizontalPadding: CGFloat = 22 + private let minimumHeight: CGFloat = 44 + + var style: Style = .regular { + didSet { + updateBackgroundColor() + updateTitleColor() + } + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + layer.cornerRadius = 8 + updateBackgroundColor() + updateTitleColor() + titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .medium) + + heightAnchor.constraint(greaterThanOrEqualToConstant: minimumHeight).isActive = true + contentEdgeInsets = UIEdgeInsets(top: 0, left: horizontalPadding, bottom: 0, right: horizontalPadding) + } + + override var isEnabled: Bool { + didSet { + updateBackgroundColor() + updateTitleColor() + } + } + + private func updateBackgroundColor() { + if isEnabled { + let enabledColor = UIColor(red: 64 / 255, green: 159 / 255, blue: 255 / 255, alpha: 1) + let alternativeColor = UIColor(white: 235 / 255, alpha: 1) + backgroundColor = style == .alternative ? alternativeColor : enabledColor + } else { + backgroundColor = UIColor(red: 165 / 255, green: 168 / 255, blue: 172 / 255, alpha: 1) + } + + } + + private func updateTitleColor() { + let titleColor = UIColor.white + let alternativeTitleColor = UIColor(white: 75 / 255, alpha: 1) + setTitleColor(style == .alternative ? alternativeTitleColor : titleColor, for: .normal) + } + } diff --git a/WeTransfer Sample Project/Supporting Files/AppDelegate.swift b/WeTransfer Sample Project/Supporting Files/AppDelegate.swift index 3462b17..8787c77 100644 --- a/WeTransfer Sample Project/Supporting Files/AppDelegate.swift +++ b/WeTransfer Sample Project/Supporting Files/AppDelegate.swift @@ -10,10 +10,10 @@ import UIKit @UIApplicationMain final class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - return true - } + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } } diff --git a/WeTransfer/Models/Board.swift b/WeTransfer/Models/Board.swift index a37cdcd..1068b22 100644 --- a/WeTransfer/Models/Board.swift +++ b/WeTransfer/Models/Board.swift @@ -11,45 +11,45 @@ import Foundation /// Describes a single board to be created, adding files to and uploading files from. Used as an identifier between each request to be made and a local representation of the server-side board. /// Files should be added through the appropriate addFiles method public final class Board: Transferable { - public private(set) var identifier: String? - - /// The name of the board. This name will be shown when viewing the transfer on wetransfer.com - public let name: String - /// Optional description of the board. This will be shown when viewing the transfer on wetransfer.com - public let description: String? - - /// References to all the files added to the board. Files can be added with the public method on the WeTransfer struct - public private(set) var files: [File] = [] - - /// Available when the board is created on the server - public private(set) var shortURL: URL? - - /// Internal initializer with required properties - init(name: String, description: String?) { - self.name = name - self.description = description - } + public private(set) var identifier: String? + + /// The name of the board. This name will be shown when viewing the transfer on wetransfer.com + public let name: String + /// Optional description of the board. This will be shown when viewing the transfer on wetransfer.com + public let description: String? + + /// References to all the files added to the board. Files can be added with the public method on the WeTransfer struct + public private(set) var files: [File] = [] + + /// Available when the board is created on the server + public private(set) var shortURL: URL? + + /// Internal initializer with required properties + init(name: String, description: String?) { + self.name = name + self.description = description + } } // MARK: - Private updating methods extension Board { - - /// Updates the board with server-side information - /// - /// - Parameters: - /// - identifier: Identifier to point to global board - /// - shortURL: URL of where the board can be found online - func update(with identifier: String, shortURL: URL) { - self.identifier = identifier - self.shortURL = shortURL - } - - /// Adds provided files to the board locally - /// - /// - Parameter files: Files to be added to the board - func add(_ files: [File]) { - for file in files where !self.files.contains(file) { - self.files.append(file) - } - } + + /// Updates the board with server-side information + /// + /// - Parameters: + /// - identifier: Identifier to point to global board + /// - shortURL: URL of where the board can be found online + func update(with identifier: String, shortURL: URL) { + self.identifier = identifier + self.shortURL = shortURL + } + + /// Adds provided files to the board locally + /// + /// - Parameter files: Files to be added to the board + func add(_ files: [File]) { + for file in files where !self.files.contains(file) { + self.files.append(file) + } + } } diff --git a/WeTransfer/Models/Chunk.swift b/WeTransfer/Models/Chunk.swift index ac91fd2..2f556c5 100644 --- a/WeTransfer/Models/Chunk.swift +++ b/WeTransfer/Models/Chunk.swift @@ -10,51 +10,51 @@ import Foundation /// Represents a chunk of data from a file in a transfer or board. Used only in the uploading proces struct Chunk: Encodable { - - /// Fallback size chunks except the last, as the last chunk holds the remaining data (filesize % defaultChunkSize) - static let defaultChunkSize: Bytes = (6 * 1024 * 1024) - - /// Zero-based index of chunk - /// - Note: Server-side these are called partNumber and start at 1 - let chunkIndex: Int - - /// File URL pointing to local file from File struct - let fileURL: URL - /// URL to upload the chunk to - let uploadURL: URL - - /// Size of the chunk in bytes - let size: Bytes - /// Offset of the chunk in bytes. This is from where to read the data from the file - let byteOffset: Bytes + + /// Fallback size chunks except the last, as the last chunk holds the remaining data (filesize % defaultChunkSize) + static let defaultChunkSize: Bytes = (6 * 1024 * 1024) + + /// Zero-based index of chunk + /// - Note: Server-side these are called partNumber and start at 1 + let chunkIndex: Int + + /// File URL pointing to local file from File struct + let fileURL: URL + /// URL to upload the chunk to + let uploadURL: URL + + /// Size of the chunk in bytes + let size: Bytes + /// Offset of the chunk in bytes. This is from where to read the data from the file + let byteOffset: Bytes } extension Chunk { - /// Initializes a chunk from its File, an index and the URL it should be uploaded to. This struct will be used to upload the file in seperate chunks. For each chunk the size and offset are calculated with the bytes available in the File - /// - /// - Parameters: - /// - file: The file for which the chunk should be created - /// - chunkIndex: The index of the chunk - /// - uploadURL: The URL to where the chunk should be uploaded - init(file: File, chunkIndex: Int, uploadURL: URL) { - let chunkSize = file.chunkSize ?? Chunk.defaultChunkSize - let byteOffset = chunkSize * Bytes(chunkIndex) - self.init(chunkIndex: chunkIndex, - fileURL: file.url, - uploadURL: uploadURL, - size: min(file.filesize - byteOffset, chunkSize), - byteOffset: byteOffset) - } + /// Initializes a chunk from its File, an index and the URL it should be uploaded to. This struct will be used to upload the file in seperate chunks. For each chunk the size and offset are calculated with the bytes available in the File + /// + /// - Parameters: + /// - file: The file for which the chunk should be created + /// - chunkIndex: The index of the chunk + /// - uploadURL: The URL to where the chunk should be uploaded + init(file: File, chunkIndex: Int, uploadURL: URL) { + let chunkSize = file.chunkSize ?? Chunk.defaultChunkSize + let byteOffset = chunkSize * Bytes(chunkIndex) + self.init(chunkIndex: chunkIndex, + fileURL: file.url, + uploadURL: uploadURL, + size: min(file.filesize - byteOffset, chunkSize), + byteOffset: byteOffset) + } } extension Data { - /// Initializes a Data object pointing to the correct bytes in the file of the chunk - /// - /// - Parameter chunk: The chunk with the information about the file, the size and the byte offset - /// - Throws: Any error thrown from initializing the FileHandle - init(from chunk: Chunk) throws { - let file = try FileHandle(forReadingFrom: chunk.fileURL) - file.seek(toFileOffset: UInt64(chunk.byteOffset)) - self = file.readData(ofLength: Int(chunk.size)) - } + /// Initializes a Data object pointing to the correct bytes in the file of the chunk + /// + /// - Parameter chunk: The chunk with the information about the file, the size and the byte offset + /// - Throws: Any error thrown from initializing the FileHandle + init(from chunk: Chunk) throws { + let file = try FileHandle(forReadingFrom: chunk.fileURL) + file.seek(toFileOffset: UInt64(chunk.byteOffset)) + self = file.readData(ofLength: Int(chunk.size)) + } } diff --git a/WeTransfer/Models/File.swift b/WeTransfer/Models/File.swift index 2d8ab0a..079abc4 100644 --- a/WeTransfer/Models/File.swift +++ b/WeTransfer/Models/File.swift @@ -14,66 +14,66 @@ public typealias Bytes = UInt64 /// A file used in a Transfer or a Board. Should be initialized with a URL pointing only to a local file /// As files should be readily available for uploading, only local files accessible by NSFileManager should be used public final class File: Encodable { - - public enum Error: Swift.Error, LocalizedError { - /// Provided file URL could not be used to get file size information - case fileSizeUnavailable - - public var errorDescription: String? { - switch self { - case .fileSizeUnavailable: - return "No file size information available" - } - } - } - - /// Location of the file on disk - public let url: URL - - /// Server-side identifier when file is added to the transfer or board on the server - public private(set) var identifier: String? - - /// Will be set to yes when all chunks of the file have been uploaded - public internal(set) var isUploaded: Bool = false - - /// Name of the file. Should be the last path component of the url - public var filename: String { - return url.lastPathComponent - } - - /// Size of the file in Bytes - public let filesize: Bytes - - /// Maximum size that each chunk needs to be - public internal(set) var chunkSize: Bytes? - - public private(set) var numberOfChunks: Int? - private(set) var multipartUploadIdentifier: String? - - public init(url: URL) throws { - self.url = url - - let fileAttributes = try FileManager.default.attributesOfItem(atPath: url.path) - guard let filesizeAttribute = fileAttributes[.size] as? UInt64 else { - throw Error.fileSizeUnavailable - } - self.filesize = filesizeAttribute - } + + public enum Error: Swift.Error, LocalizedError { + /// Provided file URL could not be used to get file size information + case fileSizeUnavailable + + public var errorDescription: String? { + switch self { + case .fileSizeUnavailable: + return "No file size information available" + } + } + } + + /// Location of the file on disk + public let url: URL + + /// Server-side identifier when file is added to the transfer or board on the server + public private(set) var identifier: String? + + /// Will be set to yes when all chunks of the file have been uploaded + public internal(set) var isUploaded: Bool = false + + /// Name of the file. Should be the last path component of the url + public var filename: String { + return url.lastPathComponent + } + + /// Size of the file in Bytes + public let filesize: Bytes + + /// Maximum size that each chunk needs to be + public internal(set) var chunkSize: Bytes? + + public private(set) var numberOfChunks: Int? + private(set) var multipartUploadIdentifier: String? + + public init(url: URL) throws { + self.url = url + + let fileAttributes = try FileManager.default.attributesOfItem(atPath: url.path) + guard let filesizeAttribute = fileAttributes[.size] as? UInt64 else { + throw Error.fileSizeUnavailable + } + self.filesize = filesizeAttribute + } } extension File: Equatable { - /// Only compares the url and optional identifier of the file - /// Note: Disregards any state, so the `uploaded` property is ignored - public static func == (lhs: File, rhs: File) -> Bool { - return lhs.url == rhs.url && lhs.identifier == rhs.identifier - } + /// Only compares the url and optional identifier of the file + /// Note: Disregards any state, so the `uploaded` property is ignored + public static func == (lhs: File, rhs: File) -> Bool { + return lhs.url == rhs.url && lhs.identifier == rhs.identifier + } } extension File { - func update(with identifier: String, numberOfChunks: Int, chunkSize: Bytes, multipartUploadIdentifier: String?) { - self.identifier = identifier - self.numberOfChunks = numberOfChunks - self.chunkSize = chunkSize - self.multipartUploadIdentifier = multipartUploadIdentifier - } + func update(with identifier: String, numberOfChunks: Int, chunkSize: Bytes, multipartUploadIdentifier: String?) { + self.identifier = identifier + self.numberOfChunks = numberOfChunks + self.chunkSize = chunkSize + self.multipartUploadIdentifier = multipartUploadIdentifier + } } diff --git a/WeTransfer/Models/Transfer.swift b/WeTransfer/Models/Transfer.swift index 6ca7af3..f504885 100644 --- a/WeTransfer/Models/Transfer.swift +++ b/WeTransfer/Models/Transfer.swift @@ -10,33 +10,33 @@ import Foundation /// Describes a single transfer to be created and uploaded. Used as an identifier between each request to be made and a local representation of the server-side transfer. public final class Transfer: Transferable { - public let identifier: String? - - /// The name of the transfer. This name will be shown when viewing the transfer on wetransfer.com - public let message: String - - /// References to all the files added to the transfer - public let files: [File] - - /// Available when the transfer is created on the server - public private(set) var shortURL: URL? - - /// Internal initializer with required properties - init(identifier: String, message: String, files: [File] = []) { - self.identifier = identifier - self.message = message - self.files = files - } + public let identifier: String? + + /// The name of the transfer. This name will be shown when viewing the transfer on wetransfer.com + public let message: String + + /// References to all the files added to the transfer + public let files: [File] + + /// Available when the transfer is created on the server + public private(set) var shortURL: URL? + + /// Internal initializer with required properties + init(identifier: String, message: String, files: [File] = []) { + self.identifier = identifier + self.message = message + self.files = files + } } // MARK: - Private updating methods extension Transfer { - - /// Updates the transfer with server-side information - /// - /// - Parameters: - /// - shortURL: URL of where the transfer can be found online - func update(with shortURL: URL) { - self.shortURL = shortURL - } + + /// Updates the transfer with server-side information + /// + /// - Parameters: + /// - shortURL: URL of where the transfer can be found online + func update(with shortURL: URL) { + self.shortURL = shortURL + } } diff --git a/WeTransfer/Models/Transferable.swift b/WeTransfer/Models/Transferable.swift index 83c8b1e..6888763 100644 --- a/WeTransfer/Models/Transferable.swift +++ b/WeTransfer/Models/Transferable.swift @@ -10,7 +10,7 @@ import Foundation /// Shared properties for both transfers and boards public protocol Transferable { - var identifier: String? { get } - var files: [File] { get } - var shortURL: URL? { get } + var identifier: String? { get } + var files: [File] { get } + var shortURL: URL? { get } } diff --git a/WeTransfer/Server/APIClient.swift b/WeTransfer/Server/APIClient.swift index 17f40e5..35b6600 100644 --- a/WeTransfer/Server/APIClient.swift +++ b/WeTransfer/Server/APIClient.swift @@ -11,70 +11,70 @@ import Foundation /// Holds the local state for communicating with the API. /// Handles the creation of the appropriate requests, and holds any request-associated classes like the decoder and encoder final class APIClient { - /// The API key used for each request - var apiKey: String? - /// URL to point to the server to. Each endpoint appends its path to the base URL - var baseURL: URL? - - /// Handles the storage of the authentication bearer and adds authentication headers to requests - let authenticator = Authenticator() - - /// Main URL session used by for all requests - let urlSession: URLSession = URLSession(configuration: .default, delegate: nil, delegateQueue: nil) - - /// Main operation queue handling all operations concurrently - let operationQueue: OperationQueue = OperationQueue() - - /// Used to decode all json repsonses - let decoder: JSONDecoder = { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - return decoder - }() - - /// Used to encode all parameters to json - let encoder: JSONEncoder = { - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - return encoder - }() + /// The API key used for each request + var apiKey: String? + /// URL to point to the server to. Each endpoint appends its path to the base URL + var baseURL: URL? + + /// Handles the storage of the authentication bearer and adds authentication headers to requests + let authenticator = Authenticator() + + /// Main URL session used by for all requests + let urlSession: URLSession = URLSession(configuration: .default, delegate: nil, delegateQueue: nil) + + /// Main operation queue handling all operations concurrently + let operationQueue: OperationQueue = OperationQueue() + + /// Used to decode all json repsonses + let decoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return decoder + }() + + /// Used to encode all parameters to json + let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + return encoder + }() } extension APIClient { - - /// Creates a URLRequest from an enpoint and optionally data to send along - /// - /// - Parameters: - /// - endpoint: Endpoint describing the url and HTTP method - /// - data: Optional data to add to the request body - /// - Returns: URLRequest pointing to URL with appropriate HTTP method set - /// - Throws: `WeTransfer.Error` when not configured or not authorized - func createRequest(_ endpoint: APIEndpoint, data: Data? = nil) throws -> URLRequest { - guard let apiKey = apiKey, let baseURL = baseURL else { - throw WeTransfer.Error.notConfigured - } - - var request = URLRequest(endpoint: endpoint, baseURL: baseURL, apiKey: apiKey) - request = authenticator.authenticatedRequest(from: request) - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - if let data = data { - request.httpBody = data - } - return request - } + + /// Creates a URLRequest from an enpoint and optionally data to send along + /// + /// - Parameters: + /// - endpoint: Endpoint describing the url and HTTP method + /// - data: Optional data to add to the request body + /// - Returns: URLRequest pointing to URL with appropriate HTTP method set + /// - Throws: `WeTransfer.Error` when not configured or not authorized + func createRequest(_ endpoint: APIEndpoint, data: Data? = nil) throws -> URLRequest { + guard let apiKey = apiKey, let baseURL = baseURL else { + throw WeTransfer.Error.notConfigured + } + + var request = URLRequest(endpoint: endpoint, baseURL: baseURL, apiKey: apiKey) + request = authenticator.authenticatedRequest(from: request) + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + if let data = data { + request.httpBody = data + } + return request + } } fileprivate extension URLRequest { - /// Initializes a URLRequest instance from and endpoint - /// - /// - Parameters: - /// - endpoint: Endpoint describing the url and HTTP method - /// - baseURL: URL to append the endpoint's path to - /// - apiKey: API key to add to the headers - init(endpoint: APIEndpoint, baseURL: URL, apiKey: String) { - self.init(url: endpoint.url(with: baseURL)) - httpMethod = endpoint.method.rawValue - addValue(apiKey, forHTTPHeaderField: "x-api-key") - } + /// Initializes a URLRequest instance from and endpoint + /// + /// - Parameters: + /// - endpoint: Endpoint describing the url and HTTP method + /// - baseURL: URL to append the endpoint's path to + /// - apiKey: API key to add to the headers + init(endpoint: APIEndpoint, baseURL: URL, apiKey: String) { + self.init(url: endpoint.url(with: baseURL)) + httpMethod = endpoint.method.rawValue + addValue(apiKey, forHTTPHeaderField: "x-api-key") + } } diff --git a/WeTransfer/Server/Authenticator.swift b/WeTransfer/Server/Authenticator.swift index b72d72a..b683108 100644 --- a/WeTransfer/Server/Authenticator.swift +++ b/WeTransfer/Server/Authenticator.swift @@ -10,32 +10,32 @@ import Foundation /// Responsible for adding the appropriate authentication headers to requests final class Authenticator { - - /// JWT bearer to add to request - private var bearer: String? - - /// Whether a bearer is set and thus the client is authenticated - var isAuthenticated: Bool { - return bearer != nil - } - - /// Updates the JWT bearer with a new bearer - /// - /// - Parameter bearer: New bearer - func updateBearer(_ bearer: String?) { - self.bearer = bearer - } - - /// Authenticates the provided request with the correct authorization headers if a bearer is set - /// - Note: Can be called regardless of availability of JWT bearer - /// - /// - Parameter request: The request to update - /// - Returns: An updated request with the correct authorization headers added - func authenticatedRequest(from request: URLRequest) -> URLRequest { - var request = request - if let bearer = bearer { - request.addValue("Bearer \(bearer)", forHTTPHeaderField: "Authorization") - } - return request - } + + /// JWT bearer to add to request + private var bearer: String? + + /// Whether a bearer is set and thus the client is authenticated + var isAuthenticated: Bool { + return bearer != nil + } + + /// Updates the JWT bearer with a new bearer + /// + /// - Parameter bearer: New bearer + func updateBearer(_ bearer: String?) { + self.bearer = bearer + } + + /// Authenticates the provided request with the correct authorization headers if a bearer is set + /// - Note: Can be called regardless of availability of JWT bearer + /// + /// - Parameter request: The request to update + /// - Returns: An updated request with the correct authorization headers added + func authenticatedRequest(from request: URLRequest) -> URLRequest { + var request = request + if let bearer = bearer { + request.addValue("Bearer \(bearer)", forHTTPHeaderField: "Authorization") + } + return request + } } diff --git a/WeTransfer/Server/Endpoints/APIEndpoint.swift b/WeTransfer/Server/Endpoints/APIEndpoint.swift index eb06301..dd81f4e 100644 --- a/WeTransfer/Server/Endpoints/APIEndpoint.swift +++ b/WeTransfer/Server/Endpoints/APIEndpoint.swift @@ -10,36 +10,36 @@ import Foundation /// Describes an endpoint to talk to the API with struct APIEndpoint { - - enum HTTPMethod: String { - case get = "GET" - case post = "POST" - case put = "PUT" - case delete = "DELETE" - } - - let method: HTTPMethod - let requiresAuthentication: Bool - let path: String - let responseType: Response.Type = Response.self - - /// Returns the the final URL by appending the path to the provided base URL - /// - /// - Parameter baseURL: The base URL to append the path property to - /// - Returns: URL appropriate for the endpoint - func url(with baseURL: URL) -> URL { - return baseURL.appendingPathComponent(path) - } - - /// Creates an APIEndpoint with a path - /// - /// - Parameters: - /// - method: HTTPMethod to use for the endpoint - /// - path: Relative path to be added to a base URL - /// - requiresAuthentication: Whether this endpoint requires authentication headers to be sent - init(method: HTTPMethod, path: String, requiresAuthentication: Bool = true) { - self.method = method - self.path = path - self.requiresAuthentication = requiresAuthentication - } + + enum HTTPMethod: String { + case get = "GET" + case post = "POST" + case put = "PUT" + case delete = "DELETE" + } + + let method: HTTPMethod + let requiresAuthentication: Bool + let path: String + let responseType: Response.Type = Response.self + + /// Returns the the final URL by appending the path to the provided base URL + /// + /// - Parameter baseURL: The base URL to append the path property to + /// - Returns: URL appropriate for the endpoint + func url(with baseURL: URL) -> URL { + return baseURL.appendingPathComponent(path) + } + + /// Creates an APIEndpoint with a path + /// + /// - Parameters: + /// - method: HTTPMethod to use for the endpoint + /// - path: Relative path to be added to a base URL + /// - requiresAuthentication: Whether this endpoint requires authentication headers to be sent + init(method: HTTPMethod, path: String, requiresAuthentication: Bool = true) { + self.method = method + self.path = path + self.requiresAuthentication = requiresAuthentication + } } diff --git a/WeTransfer/Server/Endpoints/Endpoints.swift b/WeTransfer/Server/Endpoints/Endpoints.swift index 312e810..136cf26 100644 --- a/WeTransfer/Server/Endpoints/Endpoints.swift +++ b/WeTransfer/Server/Endpoints/Endpoints.swift @@ -9,87 +9,87 @@ import Foundation extension APIEndpoint { - /// Sends the API key to authorize the client to use the API - /// - /// - Returns: APIEndpoint with `POST` to `/authorize` without a token in the headers, expecting a `AuthorizerResponse` as response - static func authorize() -> APIEndpoint { - return APIEndpoint(method: .post, path: "authorize", requiresAuthentication: false) - } - - /// Creates a new transfer - /// - /// - Returns: APIEndpoint with `POST` to `/transfer`, expecting a `CreateTransferResponse` as response - static func createTransfer() -> APIEndpoint { - return APIEndpoint(method: .post, path: "transfers") - } - - /// Creates a new transfer - /// - /// - Returns: APIEndpoint with `POST` to `/transfer`, expecting a `CreateTransferResponse` as response - static func createBoard() -> APIEndpoint { - return APIEndpoint(method: .post, path: "boards") - } - - /// Adds files to an existing board - /// - /// - Parameter boardIdentifier: Identifier of the board to add the files to - /// - Returns: APIEndpoint with `POST` to `/boards/{id}/items`, expecting an array of `AddFilesResponse` structs as response - static func addFiles(boardIdentifier: String) -> APIEndpoint<[AddFilesResponse]> { - return APIEndpoint<[AddFilesResponse]>(method: .post, path: "boards/\(boardIdentifier)/files") - } - - /// Requests upload info of a chunk of a file to be uploaded - /// - /// - Parameters: - /// - transferIdentifier: Identifier of the transfer containing the file - /// - fileIdentifier: Identifier of the file to get the chunk info of - /// - chunkIndex: Index of the chunk - /// - Returns: APIEndpoint with `GET` to `/transfers/files/{file-id}/upload-url/{part-number}/ - static func requestTransferUploadURL(transferIdentifier: String, fileIdentifier: String, chunkIndex: Int) -> APIEndpoint { - let partNumber = chunkIndex + 1 - return APIEndpoint(method: .get, path: "transfers/\(transferIdentifier)/files/\(fileIdentifier)/upload-url/\(partNumber)") - } - - /// Requests upload info of a chunk of a file to be uploaded - /// - /// - Parameters: - /// - boardIndentifier: dentifier of the board containing the file - /// - fileIdentifier: Identifier of the file to get the chunk info of - /// - chunkIndex: Index of the chunk - /// - multipartIdentifier: Multipart identifier of the file - /// - Returns: APIEndpoint with `GET` to `/files/{file-id}/uploads/{chunk-number}/{multipart-id}` - static func requestBoardUploadURL(boardIdentifier: String, fileIdentifier: String, chunkIndex: Int, multipartIdentifier: String) -> APIEndpoint { - let partNumber = chunkIndex + 1 - return APIEndpoint(method: .get, path: "boards/\(boardIdentifier)/files/\(fileIdentifier)/upload-url/\(partNumber)/\(multipartIdentifier)") - } - - /// Completes the upload of file, assuming all chunks have finished uploading - /// - /// - Parameters: - /// - transferIdentifier: Identifier for the containing transfer - /// - fileIdentifier: Identifier of the file - /// - Returns: APIEndpoint with `POST` to `/files/{file-id}/uploads/complete` - static func completeTransferFileUpload(transferIdentifier: String, fileIdentifier: String) -> APIEndpoint { - return APIEndpoint(method: .put, path: "transfers/\(transferIdentifier)/files/\(fileIdentifier)/upload-complete") - } - - /// Completes the upload of file, assuming all chunks have finished uploading - /// - /// - Parameters: - /// - boardIdentifier: Identifier for the containing board - /// - fileIdentifier: Identifier of the file - /// - Returns: APIEndpoint with `POST` to `/files/{file-id}/uploads/complete` - static func completeBoardFileUpload(boardIdentifier: String, fileIdentifier: String) -> APIEndpoint { - return APIEndpoint(method: .put, path: "boards/\(boardIdentifier)/files/\(fileIdentifier)/upload-complete") - } - - /// Finilizes the transfer, resulting in an URL to be added to the transfer - /// - /// - Parameter transferIdentifier: Identifier for the transfer - /// - Returns: APIEndopint with `PUT` to `transfers/{transfer-id}/finalize` - static func finalizeTransfer(transferIdentifier: String) -> APIEndpoint { - return APIEndpoint(method: .put, path: "transfers/\(transferIdentifier)/finalize") - } + /// Sends the API key to authorize the client to use the API + /// + /// - Returns: APIEndpoint with `POST` to `/authorize` without a token in the headers, expecting a `AuthorizerResponse` as response + static func authorize() -> APIEndpoint { + return APIEndpoint(method: .post, path: "authorize", requiresAuthentication: false) + } + + /// Creates a new transfer + /// + /// - Returns: APIEndpoint with `POST` to `/transfer`, expecting a `CreateTransferResponse` as response + static func createTransfer() -> APIEndpoint { + return APIEndpoint(method: .post, path: "transfers") + } + + /// Creates a new transfer + /// + /// - Returns: APIEndpoint with `POST` to `/transfer`, expecting a `CreateTransferResponse` as response + static func createBoard() -> APIEndpoint { + return APIEndpoint(method: .post, path: "boards") + } + + /// Adds files to an existing board + /// + /// - Parameter boardIdentifier: Identifier of the board to add the files to + /// - Returns: APIEndpoint with `POST` to `/boards/{id}/items`, expecting an array of `AddFilesResponse` structs as response + static func addFiles(boardIdentifier: String) -> APIEndpoint<[AddFilesResponse]> { + return APIEndpoint<[AddFilesResponse]>(method: .post, path: "boards/\(boardIdentifier)/files") + } + + /// Requests upload info of a chunk of a file to be uploaded + /// + /// - Parameters: + /// - transferIdentifier: Identifier of the transfer containing the file + /// - fileIdentifier: Identifier of the file to get the chunk info of + /// - chunkIndex: Index of the chunk + /// - Returns: APIEndpoint with `GET` to `/transfers/files/{file-id}/upload-url/{part-number}/ + static func requestTransferUploadURL(transferIdentifier: String, fileIdentifier: String, chunkIndex: Int) -> APIEndpoint { + let partNumber = chunkIndex + 1 + return APIEndpoint(method: .get, path: "transfers/\(transferIdentifier)/files/\(fileIdentifier)/upload-url/\(partNumber)") + } + + /// Requests upload info of a chunk of a file to be uploaded + /// + /// - Parameters: + /// - boardIndentifier: dentifier of the board containing the file + /// - fileIdentifier: Identifier of the file to get the chunk info of + /// - chunkIndex: Index of the chunk + /// - multipartIdentifier: Multipart identifier of the file + /// - Returns: APIEndpoint with `GET` to `/files/{file-id}/uploads/{chunk-number}/{multipart-id}` + static func requestBoardUploadURL(boardIdentifier: String, fileIdentifier: String, chunkIndex: Int, multipartIdentifier: String) -> APIEndpoint { + let partNumber = chunkIndex + 1 + return APIEndpoint(method: .get, path: "boards/\(boardIdentifier)/files/\(fileIdentifier)/upload-url/\(partNumber)/\(multipartIdentifier)") + } + + /// Completes the upload of file, assuming all chunks have finished uploading + /// + /// - Parameters: + /// - transferIdentifier: Identifier for the containing transfer + /// - fileIdentifier: Identifier of the file + /// - Returns: APIEndpoint with `POST` to `/files/{file-id}/uploads/complete` + static func completeTransferFileUpload(transferIdentifier: String, fileIdentifier: String) -> APIEndpoint { + return APIEndpoint(method: .put, path: "transfers/\(transferIdentifier)/files/\(fileIdentifier)/upload-complete") + } + + /// Completes the upload of file, assuming all chunks have finished uploading + /// + /// - Parameters: + /// - boardIdentifier: Identifier for the containing board + /// - fileIdentifier: Identifier of the file + /// - Returns: APIEndpoint with `POST` to `/files/{file-id}/uploads/complete` + static func completeBoardFileUpload(boardIdentifier: String, fileIdentifier: String) -> APIEndpoint { + return APIEndpoint(method: .put, path: "boards/\(boardIdentifier)/files/\(fileIdentifier)/upload-complete") + } + + /// Finilizes the transfer, resulting in an URL to be added to the transfer + /// + /// - Parameter transferIdentifier: Identifier for the transfer + /// - Returns: APIEndopint with `PUT` to `transfers/{transfer-id}/finalize` + static func finalizeTransfer(transferIdentifier: String) -> APIEndpoint { + return APIEndpoint(method: .put, path: "transfers/\(transferIdentifier)/finalize") + } } // MARK: - Request parameters and responses @@ -102,184 +102,184 @@ struct EmptyResponse: Decodable { /// Response from authenticate request struct AuthorizeResponse: Decodable { - /// Whether authorization has succeeded - let success: Bool - /// The JWT to use in future requests - let token: String? + /// Whether authorization has succeeded + let success: Bool + /// The JWT to use in future requests + let token: String? } // MARK: - Create transfer /// Parameters used for the create transfer request struct CreateTransferParameters: Encodable { - - struct FileParameters: Encodable { - let name: String - let size: UInt64 - } - - /// Message to go with the transfer - let message: String - /// Description of all the files to add - let files: [FileParameters] - - /// Initializes the parameters with a local transfer object - /// - /// - Parameter transfer: Transfer object to create on the server - init(message: String, files: [File]) { - self.message = message - - self.files = files.map({ file in - return FileParameters(name: file.filename, size: file.filesize) - }) - } + + struct FileParameters: Encodable { + let name: String + let size: UInt64 + } + + /// Message to go with the transfer + let message: String + /// Description of all the files to add + let files: [FileParameters] + + /// Initializes the parameters with a local transfer object + /// + /// - Parameter transfer: Transfer object to create on the server + init(message: String, files: [File]) { + self.message = message + + self.files = files.map({ file in + return FileParameters(name: file.filename, size: file.filesize) + }) + } } /// Response from create transfer request struct CreateTransferResponse: Decodable { - struct FileResponse: Decodable { - /// Multipart upload information about each chunk - struct MultipartUploadInfo: Decodable { // swiftlint:disable:this nesting - /// Amount of chunks to be created - let partNumbers: Int - /// Default size for each chunk - let chunkSize: Bytes - } - - let identifier: String - /// Full name of file (e.g. "photo.jpg") - let name: String - // Size of the file in bytes - let size: Bytes - /// Mulitpart information about each chunk - let multipartUploadInfo: MultipartUploadInfo - - private enum CodingKeys: String, CodingKey { - case identifier = "id" - case name - case size - case multipartUploadInfo = "multipart" - } - } - - /// Server side identifier of the transfer - let id: String // swiftlint:disable:this identifier_name - - /// Server side information about the files - let files: [FileResponse] + struct FileResponse: Decodable { + /// Multipart upload information about each chunk + struct MultipartUploadInfo: Decodable { // swiftlint:disable:this nesting + /// Amount of chunks to be created + let partNumbers: Int + /// Default size for each chunk + let chunkSize: Bytes + } + + let identifier: String + /// Full name of file (e.g. "photo.jpg") + let name: String + // Size of the file in bytes + let size: Bytes + /// Mulitpart information about each chunk + let multipartUploadInfo: MultipartUploadInfo + + private enum CodingKeys: String, CodingKey { + case identifier = "id" + case name + case size + case multipartUploadInfo = "multipart" + } + } + + /// Server side identifier of the transfer + let id: String // swiftlint:disable:this identifier_name + + /// Server side information about the files + let files: [FileResponse] } // MARK: - Create board /// Parameters used for the create transfer request struct CreateBoardParameters: Encodable { - - struct FileParameters: Encodable { - let name: String - let size: UInt64 - } - /// Name of the transfer to create - let name: String - /// Description of the transfer to create - let description: String? - - /// Initializes the parameters with a local transfer object - /// - /// - Parameter transfer: Transfer object to create on the server - init(with board: Board) { - name = board.name - description = board.description - } + + struct FileParameters: Encodable { + let name: String + let size: UInt64 + } + /// Name of the transfer to create + let name: String + /// Description of the transfer to create + let description: String? + + /// Initializes the parameters with a local transfer object + /// + /// - Parameter transfer: Transfer object to create on the server + init(with board: Board) { + name = board.name + description = board.description + } } /// Response from create transfer request struct CreateBoardResponse: Decodable { - /// Server side identifier of the transfer - let id: String // swiftlint:disable:this identifier_name - /// The URL to where the transfer can be found online - let url: URL + /// Server side identifier of the transfer + let id: String // swiftlint:disable:this identifier_name + /// The URL to where the transfer can be found online + let url: URL } // MARK: - Add files /// Parameters used for the add files request struct AddFilesParameters: Encodable { - /// Describes a file to be added to a board - struct FileParameters: Encodable { - /// Full name of file (e.g. "photo.jpg") - let name: String - /// Filesize in bytes - let size: UInt64 - - /// Initializes Item struct with a File struct - /// - /// - Parameter file: File struct to initialize Item from - init(with file: File) { - name = file.filename - size = file.filesize - } - } - - /// All items to be added to the transfer - let files: [FileParameters] - - /// Initalizes the parameters with an array of File structs - /// - /// - Parameter files: Array of File structs to be added to the transfer - init(with files: [File]) { - self.files = files.map { file in - return FileParameters(with: file) - } - } + /// Describes a file to be added to a board + struct FileParameters: Encodable { + /// Full name of file (e.g. "photo.jpg") + let name: String + /// Filesize in bytes + let size: UInt64 + + /// Initializes Item struct with a File struct + /// + /// - Parameter file: File struct to initialize Item from + init(with file: File) { + name = file.filename + size = file.filesize + } + } + + /// All items to be added to the transfer + let files: [FileParameters] + + /// Initalizes the parameters with an array of File structs + /// + /// - Parameter files: Array of File structs to be added to the transfer + init(with files: [File]) { + self.files = files.map { file in + return FileParameters(with: file) + } + } } /// Response from the add files request struct AddFilesResponse: Decodable { - /// Contains information about the chunks and the upload identifier - struct UploadInfo: Decodable { - let id: String - let partNumbers: Int - let chunkSize: UInt64 - } - - /// Identifier of the File on the server - let id: String - /// Name of the file - let name: String - /// Size of the file in bytes - let size: Bytes - /// Upload info for the file - let multipart: UploadInfo + /// Contains information about the chunks and the upload identifier + struct UploadInfo: Decodable { + let id: String + let partNumbers: Int + let chunkSize: UInt64 + } + + /// Identifier of the File on the server + let id: String + /// Name of the file + let name: String + /// Size of the file in bytes + let size: Bytes + /// Upload info for the file + let multipart: UploadInfo } // MARK: - Request upload URL /// Response from the add upload url request for chunks struct AddUploadURLResponse: Decodable { - /// URL to upload the chunk to - let url: URL + /// URL to upload the chunk to + let url: URL } // MARK: - Complete upload /// Parameters used for the complete file upload request struct CompleteTransferFileUploadParameters: Encodable { - /// Number of chunks used for the file - let partNumbers: Int + /// Number of chunks used for the file + let partNumbers: Int } /// Response from complete upload request struct CompleteBoardFileUploadResponse: Decodable { - /// Whether the upload of all the chunks has succeeded - let success: Bool - /// Message describing either success or failure of chunk uploads - let message: String + /// Whether the upload of all the chunks has succeeded + let success: Bool + /// Message describing either success or failure of chunk uploads + let message: String } // MARK: - Finalize transfer /// Response for the finalize transfer call struct FinalizeTransferResponse: Decodable { - /// Public URL of the finalized transfer - let url: URL + /// Public URL of the finalized transfer + let url: URL } diff --git a/WeTransfer/Server/Methods/AddFiles.swift b/WeTransfer/Server/Methods/AddFiles.swift index 6fbfca6..c3b3e70 100644 --- a/WeTransfer/Server/Methods/AddFiles.swift +++ b/WeTransfer/Server/Methods/AddFiles.swift @@ -9,34 +9,34 @@ import Foundation extension WeTransfer { - - /// Adds the given files to the provided board object and on the server side as well. When succeeded the files will be updated with the appropriate data like identifiers and information about the chunks - /// Creates a remote instance of the board when that has not happended yet - /// - /// - Parameters: - /// - files: File representations to be added to the transfer - /// - board: Board object to add the files to - /// - completion: Closure to be executed when request has completed - /// - result: Result with either the updated transfer object or an error when something went wrong - public static func add(_ files: [File], to board: Board, completion: @escaping (_ result: Result) -> Void) { - let operation = AddFilesOperation(board: board, files: files) - operation.onResult = { result in - DispatchQueue.main.async { - completion(result) - } - } - - // Externally create the board if not already in the queue - if board.identifier == nil && client.operationQueue.operations.first(where: { $0 is CreateBoardOperation }) == nil { - let createBoardOperation = CreateBoardOperation(board: board) - operation.addDependency(createBoardOperation) - client.operationQueue.addOperation(createBoardOperation) - } - - // Add the latest AddFilesOperation in the queue as a dependency so all files are added in the correct order - if let queuedAddFilesOperation = client.operationQueue.operations.last(where: { $0 is AddFilesOperation}) { - operation.addDependency(queuedAddFilesOperation) - } - client.operationQueue.addOperation(operation) - } + + /// Adds the given files to the provided board object and on the server side as well. When succeeded the files will be updated with the appropriate data like identifiers and information about the chunks + /// Creates a remote instance of the board when that has not happended yet + /// + /// - Parameters: + /// - files: File representations to be added to the transfer + /// - board: Board object to add the files to + /// - completion: Closure to be executed when request has completed + /// - result: Result with either the updated transfer object or an error when something went wrong + public static func add(_ files: [File], to board: Board, completion: @escaping (_ result: Result) -> Void) { + let operation = AddFilesOperation(board: board, files: files) + operation.onResult = { result in + DispatchQueue.main.async { + completion(result) + } + } + + // Externally create the board if not already in the queue + if board.identifier == nil && client.operationQueue.operations.first(where: { $0 is CreateBoardOperation }) == nil { + let createBoardOperation = CreateBoardOperation(board: board) + operation.addDependency(createBoardOperation) + client.operationQueue.addOperation(createBoardOperation) + } + + // Add the latest AddFilesOperation in the queue as a dependency so all files are added in the correct order + if let queuedAddFilesOperation = client.operationQueue.operations.last(where: { $0 is AddFilesOperation}) { + operation.addDependency(queuedAddFilesOperation) + } + client.operationQueue.addOperation(operation) + } } diff --git a/WeTransfer/Server/Methods/Authorize.swift b/WeTransfer/Server/Methods/Authorize.swift index 90b9254..ba47de6 100644 --- a/WeTransfer/Server/Methods/Authorize.swift +++ b/WeTransfer/Server/Methods/Authorize.swift @@ -9,40 +9,40 @@ import Foundation extension WeTransfer { - - /// Authorizes the current user with the configured API key - /// - /// - Parameter completion: Executes when either succeeded or failed - /// - Parameter result: Result with empty value when succeeded, or error when failed - static func authorize(completion: @escaping (_ result: Result) -> Void) { - - let callCompletion = { result in - DispatchQueue.main.async { - completion(result) - } - } - - guard !client.authenticator.isAuthenticated else { - callCompletion(.success(())) - return - } - - request(.authorize()) { result in - switch result { - case .failure(let error): - guard case RequestError.serverError(_, _) = error else { - callCompletion(.failure(error)) - return - } - callCompletion(.failure(WeTransfer.RequestError.authorizationFailed)) - case .success(let response): - if let token = response.token, response.success { - client.authenticator.updateBearer(token) - callCompletion(.success(())) - } else { - callCompletion(.failure(RequestError.authorizationFailed)) - } - } - } - } + + /// Authorizes the current user with the configured API key + /// + /// - Parameter completion: Executes when either succeeded or failed + /// - Parameter result: Result with empty value when succeeded, or error when failed + static func authorize(completion: @escaping (_ result: Result) -> Void) { + + let callCompletion = { result in + DispatchQueue.main.async { + completion(result) + } + } + + guard !client.authenticator.isAuthenticated else { + callCompletion(.success(())) + return + } + + request(.authorize()) { result in + switch result { + case .failure(let error): + guard case RequestError.serverError(_, _) = error else { + callCompletion(.failure(error)) + return + } + callCompletion(.failure(WeTransfer.RequestError.authorizationFailed)) + case .success(let response): + if let token = response.token, response.success { + client.authenticator.updateBearer(token) + callCompletion(.success(())) + } else { + callCompletion(.failure(RequestError.authorizationFailed)) + } + } + } + } } diff --git a/WeTransfer/Server/Methods/CreateBoard.swift b/WeTransfer/Server/Methods/CreateBoard.swift index 61182cf..22bba01 100644 --- a/WeTransfer/Server/Methods/CreateBoard.swift +++ b/WeTransfer/Server/Methods/CreateBoard.swift @@ -9,24 +9,24 @@ import Foundation extension WeTransfer { - - /// Creates a board on the server and provides the given transfer object with an identifier and URL when succceeded. - /// - /// - Parameters: - /// - board: Local instance of the board to be created on the server - /// - completion: Closure that will be executed when the request or requests have finished - /// - result: Result with either the updated transfer object or an error when something went wrong - static func createExternalBoard(_ board: Board, completion: @escaping (_ result: Result) -> Void) { - - let callCompletion = { result in - DispatchQueue.main.async { - completion(result) - } - } - - let creationOperation = CreateBoardOperation(board: board) - - creationOperation.onResult = callCompletion - client.operationQueue.addOperation(creationOperation) - } + + /// Creates a board on the server and provides the given transfer object with an identifier and URL when succceeded. + /// + /// - Parameters: + /// - board: Local instance of the board to be created on the server + /// - completion: Closure that will be executed when the request or requests have finished + /// - result: Result with either the updated transfer object or an error when something went wrong + static func createExternalBoard(_ board: Board, completion: @escaping (_ result: Result) -> Void) { + + let callCompletion = { result in + DispatchQueue.main.async { + completion(result) + } + } + + let creationOperation = CreateBoardOperation(board: board) + + creationOperation.onResult = callCompletion + client.operationQueue.addOperation(creationOperation) + } } diff --git a/WeTransfer/Server/Methods/CreateTransfer.swift b/WeTransfer/Server/Methods/CreateTransfer.swift index 814c965..9e16a2e 100644 --- a/WeTransfer/Server/Methods/CreateTransfer.swift +++ b/WeTransfer/Server/Methods/CreateTransfer.swift @@ -9,27 +9,27 @@ import Foundation extension WeTransfer { - - /// Creates a transfer on the server and provides the given transfer object with an identifier and URL when succceeded. - /// If the transfer object was initialized with files, the files will be added on the server as well and updated with the appropriate data - /// - /// - Parameters: - /// - message: Message to add to transfer - /// - fileURLs: URLs pointing to local files - /// - transfer: Transfer object that should be created on the server as well - /// - completion: Closure that will be executed when the request or requests have finished - /// - result: Result with either the updated transfer object or an error when something went wrong - public static func createTransfer(saying message: String, fileURLs: [URL], completion: @escaping (_ result: Result) -> Void) { - - let callCompletion = { result in - DispatchQueue.main.async { - completion(result) - } - } - - let creationOperation = CreateTransferOperation(message: message, fileURLs: fileURLs) - - creationOperation.onResult = callCompletion - client.operationQueue.addOperation(creationOperation) - } + + /// Creates a transfer on the server and provides the given transfer object with an identifier and URL when succceeded. + /// If the transfer object was initialized with files, the files will be added on the server as well and updated with the appropriate data + /// + /// - Parameters: + /// - message: Message to add to transfer + /// - fileURLs: URLs pointing to local files + /// - transfer: Transfer object that should be created on the server as well + /// - completion: Closure that will be executed when the request or requests have finished + /// - result: Result with either the updated transfer object or an error when something went wrong + public static func createTransfer(saying message: String, fileURLs: [URL], completion: @escaping (_ result: Result) -> Void) { + + let callCompletion = { result in + DispatchQueue.main.async { + completion(result) + } + } + + let creationOperation = CreateTransferOperation(message: message, fileURLs: fileURLs) + + creationOperation.onResult = callCompletion + client.operationQueue.addOperation(creationOperation) + } } diff --git a/WeTransfer/Server/Methods/Request.swift b/WeTransfer/Server/Methods/Request.swift index 9d40583..2eadb49 100644 --- a/WeTransfer/Server/Methods/Request.swift +++ b/WeTransfer/Server/Methods/Request.swift @@ -9,128 +9,128 @@ import Foundation extension WeTransfer { - - /// General errors that can be returned from all requests - public enum RequestError: Swift.Error, LocalizedError { - /// Response returned from server could not be parsed - case invalidResponseData - /// Provided API key is not valid - case authorizationFailed - /// Error returned by server - /// - errorMessage: Description of error - /// - httpCode: The http status code of server response, if available - case serverError(errorMessage: String, httpCode: Int?) - - public var errorDescription: String? { - switch self { - case .invalidResponseData: - return "Invalid response data: Server returned unrecognized response" - case .authorizationFailed: - return "Authorization failed: Invalid API key used for request" - case .serverError(let message, let httpCode): - return "Server error \(httpCode ?? 0): \(message)" - } - } - } - - /// Response returned by server when request could not be completed - struct ErrorResponse: Decodable { - /// Whether the request has succeeded (typically `false`) - let success: Bool? - /// Message describing the error returned from the API - let message: String? - /// Actual error message from the server - let error: String? - - /// String using either the message or the error property - var errorString: String { - return (message ?? error) ?? "" - } - } - - /// Tries to create an error from the server response if decoding of expected response failed - /// - /// - Parameters: - /// - data: Data of the response - /// - urlResponse: Response description, from which a status code can be read - /// - Returns: An error if type RequestError.serverEror if error response could be parsed - static func parseErrorResponse(_ data: Data?, urlResponse: HTTPURLResponse?) -> Swift.Error? { - guard let data = data, - let errorResponse = try? client.decoder.decode(ErrorResponse.self, from: data), - errorResponse.success != true else { - return nil - } - return RequestError.serverError(errorMessage: errorResponse.errorString, httpCode: urlResponse?.statusCode) - } - - /// Creates and performs a request to the given endpoint with the provided encodable Parameters. The response of the request will be decoded to Response type, set by declaring the result in the completion closure - /// - /// - Parameters: - /// - endpoint: The Endpoint containing the url and HTTP method for the request - /// - parameters: Decodable parameters to send along with the request - /// - completion: Closure called when either request has failed, or succeeded with the decoded Response type - /// - result: Result with either the decoded Response or and error describing where the request went wrong - static func request(_ endpoint: APIEndpoint, parameters: Parameters, completion: @escaping (_ result: Result) -> Void) { - do { - let encodedData = try client.encoder.encode(parameters) - request(endpoint, data: encodedData, completion: completion) - } catch { - completion(.failure(error)) - return - } - } - - /// Creates and performs a request to the given endpoint with the optionally provided data. The response of the request will be decoded to Response type, set by declaring the result in the completion closure - /// - /// - Parameters: - /// - endpoint: The Endpoint containing the url and HTTP method for the request - /// - data: The encoded data to be sent as parameters along with the request - /// - completion: Closure called when either request has failed, or succeeded with the decoded Response type - /// - result: Result with either the decoded Response or and error describing where the request went wrong - static func request(_ endpoint: APIEndpoint, data: Data? = nil, completion: @escaping (_ result: Result) -> Void) { - - guard !endpoint.requiresAuthentication || client.authenticator.isAuthenticated else { - // Try to authenticate once, after which the authenticationBearer *should* be set - authorize { result in - if case .failure(let error) = result { - completion(.failure(error)) - return - } - // Just in case the authenticationBearer isn't set, make sure the authorize request doesn't happen endlessly - if !client.authenticator.isAuthenticated { - completion(.failure(Error.notAuthorized)) - return - } - request(endpoint, data: data, completion: completion) - } - return - } - - // Create the request with the enpoint and optional data - let urlRequest: URLRequest - do { - urlRequest = try client.createRequest(endpoint, data: data) - } catch { - completion(.failure(error)) - return - } - - // Create and start a dataTask, after which the reponse is decoded to the Response type - let task = client.urlSession.dataTask(with: urlRequest, completionHandler: { (data, urlResponse, error) in - do { - if let error = error { - throw error - } - guard let data = data else { - throw RequestError.invalidResponseData - } - let response = try client.decoder.decode(endpoint.responseType, from: data) - completion(.success(response)) - } catch { - let serverError = parseErrorResponse(data, urlResponse: urlResponse as? HTTPURLResponse) ?? error - completion(.failure(serverError)) - } - }) - task.resume() - } + + /// General errors that can be returned from all requests + public enum RequestError: Swift.Error, LocalizedError { + /// Response returned from server could not be parsed + case invalidResponseData + /// Provided API key is not valid + case authorizationFailed + /// Error returned by server + /// - errorMessage: Description of error + /// - httpCode: The http status code of server response, if available + case serverError(errorMessage: String, httpCode: Int?) + + public var errorDescription: String? { + switch self { + case .invalidResponseData: + return "Invalid response data: Server returned unrecognized response" + case .authorizationFailed: + return "Authorization failed: Invalid API key used for request" + case .serverError(let message, let httpCode): + return "Server error \(httpCode ?? 0): \(message)" + } + } + } + + /// Response returned by server when request could not be completed + struct ErrorResponse: Decodable { + /// Whether the request has succeeded (typically `false`) + let success: Bool? + /// Message describing the error returned from the API + let message: String? + /// Actual error message from the server + let error: String? + + /// String using either the message or the error property + var errorString: String { + return (message ?? error) ?? "" + } + } + + /// Tries to create an error from the server response if decoding of expected response failed + /// + /// - Parameters: + /// - data: Data of the response + /// - urlResponse: Response description, from which a status code can be read + /// - Returns: An error if type RequestError.serverEror if error response could be parsed + static func parseErrorResponse(_ data: Data?, urlResponse: HTTPURLResponse?) -> Swift.Error? { + guard let data = data, + let errorResponse = try? client.decoder.decode(ErrorResponse.self, from: data), + errorResponse.success != true else { + return nil + } + return RequestError.serverError(errorMessage: errorResponse.errorString, httpCode: urlResponse?.statusCode) + } + + /// Creates and performs a request to the given endpoint with the provided encodable Parameters. The response of the request will be decoded to Response type, set by declaring the result in the completion closure + /// + /// - Parameters: + /// - endpoint: The Endpoint containing the url and HTTP method for the request + /// - parameters: Decodable parameters to send along with the request + /// - completion: Closure called when either request has failed, or succeeded with the decoded Response type + /// - result: Result with either the decoded Response or and error describing where the request went wrong + static func request(_ endpoint: APIEndpoint, parameters: Parameters, completion: @escaping (_ result: Result) -> Void) { + do { + let encodedData = try client.encoder.encode(parameters) + request(endpoint, data: encodedData, completion: completion) + } catch { + completion(.failure(error)) + return + } + } + + /// Creates and performs a request to the given endpoint with the optionally provided data. The response of the request will be decoded to Response type, set by declaring the result in the completion closure + /// + /// - Parameters: + /// - endpoint: The Endpoint containing the url and HTTP method for the request + /// - data: The encoded data to be sent as parameters along with the request + /// - completion: Closure called when either request has failed, or succeeded with the decoded Response type + /// - result: Result with either the decoded Response or and error describing where the request went wrong + static func request(_ endpoint: APIEndpoint, data: Data? = nil, completion: @escaping (_ result: Result) -> Void) { + + guard !endpoint.requiresAuthentication || client.authenticator.isAuthenticated else { + // Try to authenticate once, after which the authenticationBearer *should* be set + authorize { result in + if case .failure(let error) = result { + completion(.failure(error)) + return + } + // Just in case the authenticationBearer isn't set, make sure the authorize request doesn't happen endlessly + if !client.authenticator.isAuthenticated { + completion(.failure(Error.notAuthorized)) + return + } + request(endpoint, data: data, completion: completion) + } + return + } + + // Create the request with the enpoint and optional data + let urlRequest: URLRequest + do { + urlRequest = try client.createRequest(endpoint, data: data) + } catch { + completion(.failure(error)) + return + } + + // Create and start a dataTask, after which the reponse is decoded to the Response type + let task = client.urlSession.dataTask(with: urlRequest, completionHandler: { (data, urlResponse, error) in + do { + if let error = error { + throw error + } + guard let data = data else { + throw RequestError.invalidResponseData + } + let response = try client.decoder.decode(endpoint.responseType, from: data) + completion(.success(response)) + } catch { + let serverError = parseErrorResponse(data, urlResponse: urlResponse as? HTTPURLResponse) ?? error + completion(.failure(serverError)) + } + }) + task.resume() + } } diff --git a/WeTransfer/Server/Methods/Upload.swift b/WeTransfer/Server/Methods/Upload.swift index bcfda75..8445cd4 100644 --- a/WeTransfer/Server/Methods/Upload.swift +++ b/WeTransfer/Server/Methods/Upload.swift @@ -9,74 +9,74 @@ import Foundation extension WeTransfer { - - /// State of current transfer - public enum State { - /// Object is created server side, in case of Board now has url available - case created(T) - /// Upload has started, track progress with progress object - case uploading(Progress) - /// Transfer is completed - case completed(T) - /// Transfer failed due to provided error - case failed(Swift.Error) - } - - /// Uploads the files of the provided transfer, assuming it's created on the server and has files to be uploaded - /// - /// - Parameters: - /// - transfer: The Transfer object to be sent - /// - stateChanged: Enum describing the current transfer's state. See the `State` enum description for more details for each state - public static func upload(_ transfer: Transfer, stateChanged: @escaping (State) -> Void) { - - let changeState = { result in - DispatchQueue.main.async { - stateChanged(result) - } - } - - let uploadOperation = UploadFilesOperation(container: transfer) - changeState(.uploading(uploadOperation.progress)) - - let finalizeOperation = FinalizeTransferOperation() - - finalizeOperation.onResult = { result in - switch result { - case .failure(let error): - changeState(.failed(error)) - case .success(let transfer): - changeState(.completed(transfer)) - } - } - - let operations = [uploadOperation, finalizeOperation].chained() - client.operationQueue.addOperations(operations, waitUntilFinished: false) - } - - /// Uploads the files of the provided board, assuming it's created on the server and has files to be uploaded - /// - /// - Parameters: - /// - board: The Board object to upload files from - /// - stateChanged: Enum describing the state of the upload process. See the `State` enum description for more details for each state - public static func upload(_ board: Board, stateChanged: @escaping (State) -> Void) { - - let changeState = { result in - DispatchQueue.main.async { - stateChanged(result) - } - } - - let operation = UploadFilesOperation(container: board) - changeState(.uploading(operation.progress)) - - operation.onResult = { result in - switch result { - case .failure(let error): - changeState(.failed(error)) - case .success(let board): - changeState(.completed(board)) - } - } - client.operationQueue.addOperation(operation) - } + + /// State of current transfer + public enum State { + /// Object is created server side, in case of Board now has url available + case created(T) + /// Upload has started, track progress with progress object + case uploading(Progress) + /// Transfer is completed + case completed(T) + /// Transfer failed due to provided error + case failed(Swift.Error) + } + + /// Uploads the files of the provided transfer, assuming it's created on the server and has files to be uploaded + /// + /// - Parameters: + /// - transfer: The Transfer object to be sent + /// - stateChanged: Enum describing the current transfer's state. See the `State` enum description for more details for each state + public static func upload(_ transfer: Transfer, stateChanged: @escaping (State) -> Void) { + + let changeState = { result in + DispatchQueue.main.async { + stateChanged(result) + } + } + + let uploadOperation = UploadFilesOperation(container: transfer) + changeState(.uploading(uploadOperation.progress)) + + let finalizeOperation = FinalizeTransferOperation() + + finalizeOperation.onResult = { result in + switch result { + case .failure(let error): + changeState(.failed(error)) + case .success(let transfer): + changeState(.completed(transfer)) + } + } + + let operations = [uploadOperation, finalizeOperation].chained() + client.operationQueue.addOperations(operations, waitUntilFinished: false) + } + + /// Uploads the files of the provided board, assuming it's created on the server and has files to be uploaded + /// + /// - Parameters: + /// - board: The Board object to upload files from + /// - stateChanged: Enum describing the state of the upload process. See the `State` enum description for more details for each state + public static func upload(_ board: Board, stateChanged: @escaping (State) -> Void) { + + let changeState = { result in + DispatchQueue.main.async { + stateChanged(result) + } + } + + let operation = UploadFilesOperation(container: board) + changeState(.uploading(operation.progress)) + + operation.onResult = { result in + switch result { + case .failure(let error): + changeState(.failed(error)) + case .success(let board): + changeState(.completed(board)) + } + } + client.operationQueue.addOperation(operation) + } } diff --git a/WeTransfer/Server/Operations/Abstract/AsynchronousDependencyResultOperation.swift b/WeTransfer/Server/Operations/Abstract/AsynchronousDependencyResultOperation.swift index d3e934b..db7d645 100644 --- a/WeTransfer/Server/Operations/Abstract/AsynchronousDependencyResultOperation.swift +++ b/WeTransfer/Server/Operations/Abstract/AsynchronousDependencyResultOperation.swift @@ -10,21 +10,21 @@ import Foundation /// An asynchronous operation which will always have a result after completion. class AsynchronousDependencyResultOperation: AsynchronousResultOperation { // swiftlint:disable:next final_class - - override func execute() { - let resultDependencies = dependencies.compactMap({ $0 as? AsynchronousResultOperation }) - - let errors = resultDependencies.compactMap({ $0.result?.error }) - let results = resultDependencies.compactMap({ $0.result?.value }) - - // For now, both the last error or the last result are used from all dependencies. - // While this is not ideal, in the use case of this project only the last error or result is actually needed - if let error = errors.last { - finish(with: .failure(error)) - } else if let result = results.last { - finish(with: .success(result)) - } else { - finish() - } - } + + override func execute() { + let resultDependencies = dependencies.compactMap({ $0 as? AsynchronousResultOperation }) + + let errors = resultDependencies.compactMap({ $0.result?.error }) + let results = resultDependencies.compactMap({ $0.result?.value }) + + // For now, both the last error or the last result are used from all dependencies. + // While this is not ideal, in the use case of this project only the last error or result is actually needed + if let error = errors.last { + finish(with: .failure(error)) + } else if let result = results.last { + finish(with: .success(result)) + } else { + finish() + } + } } diff --git a/WeTransfer/Server/Operations/Abstract/AsynchronousOperation.swift b/WeTransfer/Server/Operations/Abstract/AsynchronousOperation.swift index bbd4c68..322bc9c 100644 --- a/WeTransfer/Server/Operations/Abstract/AsynchronousOperation.swift +++ b/WeTransfer/Server/Operations/Abstract/AsynchronousOperation.swift @@ -10,153 +10,153 @@ import Foundation /// A base class to handle NSOperation states, because this is quite verbose due to KVO and the combined attribute states. class AsynchronousOperation: Operation { // swiftlint:disable:this final_class - - // MARK: - Types - - /// An option set to express all the possible Operation states. - private struct State: OptionSet { - let rawValue: Int - static let executing = State(rawValue: 1 << 0) - static let finished = State(rawValue: 1 << 1) - static let cancelled = State(rawValue: 1 << 2) - - var executing: Bool { - get { - return contains(.executing) - } - set { - if newValue { - insert(.executing) - } else { - remove(.executing) - } - } - } - - var finished: Bool { - get { - return contains(.finished) - } - set { - if newValue { - insert(.finished) - } else { - remove(.finished) - } - } - } - - var cancelled: Bool { - get { - return contains(.cancelled) - } - set { - if newValue { - insert(.cancelled) - } else { - remove(.cancelled) - } - } - } - } - - // MARK: - State - - /// The dispatch queue that's used for mutating and reading the operation state. The state should be able to be read by multiple threads at once, but should obviously only be mutated by 1 thread at a time. - private let stateQueue = DispatchQueue(label: "com.wetransfer.swiftsdk.dataoperation.state", attributes: [.concurrent]) - - /// A private option set to define the operation state. Should only be mutated by the dedicated setters in AsynchronousOperation, to guarantee thread-safety. - private var rawState = State() - - /// A thread safe overridden isExecuting property from NSOperation. Returns whether the operation is currently executing its tasks. This is fully managed by the AsynchronousOperation class. - private(set) public final override var isExecuting: Bool { - get { - return stateQueue.sync { - return rawState.executing - } - } - set { - willChangeValue(forKey: "isExecuting") - stateQueue.sync(flags: [.barrier]) { - rawState.executing = newValue - } - didChangeValue(forKey: "isExecuting") - } - } - - /// A thread safe overridden isFinished property from NSOperation. Returns whether the operation is done with its task. This is fully managed by the AsynchronousOperation class. - private(set) public final override var isFinished: Bool { - get { - return stateQueue.sync { - return rawState.finished - } - } - set { - willChangeValue(forKey: "isFinished") - stateQueue.sync(flags: [.barrier]) { - rawState.finished = newValue - } - didChangeValue(forKey: "isFinished") - } - } - - /// A thread safe overridden isCancelled property from NSOperation. Returns whether the operation has been cancelled. This is fully managed by the AsynchronousOperation class. - private(set) public final override var isCancelled: Bool { - get { - return stateQueue.sync { - return rawState.cancelled - } - } - set { - willChangeValue(forKey: "isCancelled") - stateQueue.sync(flags: [.barrier]) { - rawState.cancelled = newValue - } - didChangeValue(forKey: "isCancelled") - } - } - - // MARK: - Operation - - /// Overridden method from NSOperation. - public final override var isAsynchronous: Bool { - return true - } - - /// Overridden method from NSOperation. Starts executing the operation work. This is final, because subclasses should use the execute() method instead. - override func start() { - super.start() - - guard !isCancelled else { - finish() - return - } - - isFinished = false - isExecuting = true - execute() - } - - /// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception. - func execute() { - fatalError("Subclasses must implement `execute` without overriding super.") - } - - /// Call this function after any work is done to move the operation into a completed state. - func finish() { - isExecuting = false - isFinished = true - } - - /// Overridden cancel method from NSOperation. Cancels the current execution, if possible. - override func cancel() { - super.cancel() - isCancelled = true - - // Only finish if we're already executing. Otherwise we'll end up in a finished state while the operation has not even started. This will cause an exception and crashes the app. - if isExecuting { - isExecuting = false - isFinished = true - } - } + + // MARK: - Types + + /// An option set to express all the possible Operation states. + private struct State: OptionSet { + let rawValue: Int + static let executing = State(rawValue: 1 << 0) + static let finished = State(rawValue: 1 << 1) + static let cancelled = State(rawValue: 1 << 2) + + var executing: Bool { + get { + return contains(.executing) + } + set { + if newValue { + insert(.executing) + } else { + remove(.executing) + } + } + } + + var finished: Bool { + get { + return contains(.finished) + } + set { + if newValue { + insert(.finished) + } else { + remove(.finished) + } + } + } + + var cancelled: Bool { + get { + return contains(.cancelled) + } + set { + if newValue { + insert(.cancelled) + } else { + remove(.cancelled) + } + } + } + } + + // MARK: - State + + /// The dispatch queue that's used for mutating and reading the operation state. The state should be able to be read by multiple threads at once, but should obviously only be mutated by 1 thread at a time. + private let stateQueue = DispatchQueue(label: "com.wetransfer.swiftsdk.dataoperation.state", attributes: [.concurrent]) + + /// A private option set to define the operation state. Should only be mutated by the dedicated setters in AsynchronousOperation, to guarantee thread-safety. + private var rawState = State() + + /// A thread safe overridden isExecuting property from NSOperation. Returns whether the operation is currently executing its tasks. This is fully managed by the AsynchronousOperation class. + private(set) public final override var isExecuting: Bool { + get { + return stateQueue.sync { + return rawState.executing + } + } + set { + willChangeValue(forKey: "isExecuting") + stateQueue.sync(flags: [.barrier]) { + rawState.executing = newValue + } + didChangeValue(forKey: "isExecuting") + } + } + + /// A thread safe overridden isFinished property from NSOperation. Returns whether the operation is done with its task. This is fully managed by the AsynchronousOperation class. + private(set) public final override var isFinished: Bool { + get { + return stateQueue.sync { + return rawState.finished + } + } + set { + willChangeValue(forKey: "isFinished") + stateQueue.sync(flags: [.barrier]) { + rawState.finished = newValue + } + didChangeValue(forKey: "isFinished") + } + } + + /// A thread safe overridden isCancelled property from NSOperation. Returns whether the operation has been cancelled. This is fully managed by the AsynchronousOperation class. + private(set) public final override var isCancelled: Bool { + get { + return stateQueue.sync { + return rawState.cancelled + } + } + set { + willChangeValue(forKey: "isCancelled") + stateQueue.sync(flags: [.barrier]) { + rawState.cancelled = newValue + } + didChangeValue(forKey: "isCancelled") + } + } + + // MARK: - Operation + + /// Overridden method from NSOperation. + public final override var isAsynchronous: Bool { + return true + } + + /// Overridden method from NSOperation. Starts executing the operation work. This is final, because subclasses should use the execute() method instead. + override func start() { + super.start() + + guard !isCancelled else { + finish() + return + } + + isFinished = false + isExecuting = true + execute() + } + + /// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception. + func execute() { + fatalError("Subclasses must implement `execute` without overriding super.") + } + + /// Call this function after any work is done to move the operation into a completed state. + func finish() { + isExecuting = false + isFinished = true + } + + /// Overridden cancel method from NSOperation. Cancels the current execution, if possible. + override func cancel() { + super.cancel() + isCancelled = true + + // Only finish if we're already executing. Otherwise we'll end up in a finished state while the operation has not even started. This will cause an exception and crashes the app. + if isExecuting { + isExecuting = false + isFinished = true + } + } } diff --git a/WeTransfer/Server/Operations/Abstract/AsynchronousResultOperation.swift b/WeTransfer/Server/Operations/Abstract/AsynchronousResultOperation.swift index 899f6e8..8ee64d6 100644 --- a/WeTransfer/Server/Operations/Abstract/AsynchronousResultOperation.swift +++ b/WeTransfer/Server/Operations/Abstract/AsynchronousResultOperation.swift @@ -10,36 +10,36 @@ import Foundation /// An asynchronous operation which will always have a result after completion. class AsynchronousResultOperation: AsynchronousOperation { // swiftlint:disable:next final_class - - typealias ResultHandler = ((_ result: Result) -> Void) - - enum Error: Swift.Error { - case cancelled - } - - private(set) var result: Result? { - didSet { - guard let result = result else { - return - } - onResult?(result) - } - } - - /// The handler to call once the result is set. - var onResult: ResultHandler? - - public final override func finish() { - if isCancelled && result == nil { - result = Result.failure(Error.cancelled) - } - - assert(result != nil, "There should always be a result when finishing") - super.finish() - } - - public func finish(with result: Result) { - self.result = result - finish() - } + + typealias ResultHandler = ((_ result: Result) -> Void) + + enum Error: Swift.Error { + case cancelled + } + + private(set) var result: Result? { + didSet { + guard let result = result else { + return + } + onResult?(result) + } + } + + /// The handler to call once the result is set. + var onResult: ResultHandler? + + public final override func finish() { + if isCancelled && result == nil { + result = Result.failure(Error.cancelled) + } + + assert(result != nil, "There should always be a result when finishing") + super.finish() + } + + public func finish(with result: Result) { + self.result = result + finish() + } } diff --git a/WeTransfer/Server/Operations/Abstract/ChainedAsynchronousResultOperation.swift b/WeTransfer/Server/Operations/Abstract/ChainedAsynchronousResultOperation.swift index c014dd2..d8bab91 100644 --- a/WeTransfer/Server/Operations/Abstract/ChainedAsynchronousResultOperation.swift +++ b/WeTransfer/Server/Operations/Abstract/ChainedAsynchronousResultOperation.swift @@ -10,106 +10,106 @@ import Foundation // An asynchronous operation which is dependent on a parent operation for its input. class ChainedAsynchronousResultOperation: AsynchronousResultOperation { // swiftlint:disable:this final_class - - public enum Error: Swift.Error { - case invalidInput - } - - private(set) var input: Input? - - /// If `true`, the input value will be forwarded up on validation failure. - /// Can be used if an operation execution is optional. - /// - /// This requires the input type to be the same as the output type and validate(_input:) to throw no errors. - internal var shouldForwardValueOnInvalidInput: Bool { - return false - } - - /// If `true`, this operation requires the dependency to succeed. If the dependency failed, this operation will fail directly as well. - /// If defined `false`, the input parameter can't be nil up on execution. - internal var requiresDependencyToSucceed: Bool { - return true - } - - /// Creates a new instance of the operation using a given input. Mainly used for making testing easier, but also used for the first operation in a chain. - /// - /// - Parameter input: The input to use as a base. Setting this will ignore any input from dependencies. - init(input: Input? = nil) { - self.input = input - } - - public final override func start() { - updateInputFromDependencies() - super.start() - } - - final override public func execute() { - do { - if let error = dependencyResult?.error, requiresDependencyToSucceed { - throw error - } - - guard let input = input else { - fatalError("Input should exist at this moment of execution") - } - - guard try validate(input) else { - if shouldForwardValueOnInvalidInput, let output = input as? Output { - finish(with: Result.success(output)) - return - } - throw Error.invalidInput - } - - execute(input) - } catch { - finish(with: Result.failure(error)) - } - } - - func execute(_ input: Input) { - fatalError("Subclasses must implement `execute` without overriding super.") - } - - /// Can be used by its subclasses to add any validation to the input. - /// If the input is invalid, the `invalidInput` error will be set as the result. This can be overriden by a custom error by throwing inside this method. - /// If a custom error is thrown, the `shouldForwardValueOnInvalidInput` value will be ignored. - /// - /// This method will return `true` by default. - /// - /// - Parameter input: The input to validate. - /// - Returns: `true` if valid, otherwise `false`. - /// - Throws: An error if validation failed. Can be used to throw a custom error as failure. - func validate(_ input: Input) throws -> Bool { - return true - } + + public enum Error: Swift.Error { + case invalidInput + } + + private(set) var input: Input? + + /// If `true`, the input value will be forwarded up on validation failure. + /// Can be used if an operation execution is optional. + /// + /// This requires the input type to be the same as the output type and validate(_input:) to throw no errors. + internal var shouldForwardValueOnInvalidInput: Bool { + return false + } + + /// If `true`, this operation requires the dependency to succeed. If the dependency failed, this operation will fail directly as well. + /// If defined `false`, the input parameter can't be nil up on execution. + internal var requiresDependencyToSucceed: Bool { + return true + } + + /// Creates a new instance of the operation using a given input. Mainly used for making testing easier, but also used for the first operation in a chain. + /// + /// - Parameter input: The input to use as a base. Setting this will ignore any input from dependencies. + init(input: Input? = nil) { + self.input = input + } + + public final override func start() { + updateInputFromDependencies() + super.start() + } + + final override public func execute() { + do { + if let error = dependencyResult?.error, requiresDependencyToSucceed { + throw error + } + + guard let input = input else { + fatalError("Input should exist at this moment of execution") + } + + guard try validate(input) else { + if shouldForwardValueOnInvalidInput, let output = input as? Output { + finish(with: Result.success(output)) + return + } + throw Error.invalidInput + } + + execute(input) + } catch { + finish(with: Result.failure(error)) + } + } + + func execute(_ input: Input) { + fatalError("Subclasses must implement `execute` without overriding super.") + } + + /// Can be used by its subclasses to add any validation to the input. + /// If the input is invalid, the `invalidInput` error will be set as the result. This can be overriden by a custom error by throwing inside this method. + /// If a custom error is thrown, the `shouldForwardValueOnInvalidInput` value will be ignored. + /// + /// This method will return `true` by default. + /// + /// - Parameter input: The input to validate. + /// - Returns: `true` if valid, otherwise `false`. + /// - Throws: An error if validation failed. Can be used to throw a custom error as failure. + func validate(_ input: Input) throws -> Bool { + return true + } } extension ChainedAsynchronousResultOperation { - - /// Iterates over its dependencies and tries to fetch the input value. - /// If `input` is already set, the input from dependencies will be ignored. - private func updateInputFromDependencies() { - guard input == nil else { - return - } - input = dependencyResult?.value - } - - /// Iterates over its dependencies and tries to fetch the result value. - /// Will always get the first result matching dependency. - private var dependencyResult: Result? { - return dependencies.compactMap { dependency in - return dependency as? AsynchronousResultOperation - }.first?.result - } + + /// Iterates over its dependencies and tries to fetch the input value. + /// If `input` is already set, the input from dependencies will be ignored. + private func updateInputFromDependencies() { + guard input == nil else { + return + } + input = dependencyResult?.value + } + + /// Iterates over its dependencies and tries to fetch the result value. + /// Will always get the first result matching dependency. + private var dependencyResult: Result? { + return dependencies.compactMap { dependency in + return dependency as? AsynchronousResultOperation + }.first?.result + } } extension Array where Element == Operation { - func chained() -> [Element] { - for item in enumerated() where item.offset > 0 { - item.element.addDependency(self[item.offset - 1]) - } - return self - } + func chained() -> [Element] { + for item in enumerated() where item.offset > 0 { + item.element.addDependency(self[item.offset - 1]) + } + return self + } } diff --git a/WeTransfer/Server/Operations/AddFilesOperation.swift b/WeTransfer/Server/Operations/AddFilesOperation.swift index 59b81b8..fa7ff84 100644 --- a/WeTransfer/Server/Operations/AddFilesOperation.swift +++ b/WeTransfer/Server/Operations/AddFilesOperation.swift @@ -11,70 +11,70 @@ import Foundation /// Operation responsible for adding files to the provided board object and on the server as well. When succeeded, the files will be updated with the appropriate data like identifiers and information about the chunks. /// - Note: The files will be added to the provided board object when the operation has started executing final class AddFilesOperation: ChainedAsynchronousResultOperation { - - enum Error: Swift.Error, LocalizedError { - /// Not all files or incorrect file data returned by server - case incompleteFileDataReceived - - var localizedDescription: String { - switch self { - case .incompleteFileDataReceived: - return "Server did not create the correct files" - } - } - } - - /// The files to be added to the transfer if added during the initialization - private var filesToAdd: [File]? - - /// Initializes the operation with a transfer object and array of files to add. When initalized as part of a chain after `CreateTransferOperation`, this operation can be initialized without any arguments - /// - /// - Parameters: - /// - transfer: Transfer object to add the files to - /// - files: Files to be added to the transfer - convenience init(board: Board, files: [File]) { - self.init(input: board) - filesToAdd = files - } - - override func execute(_ board: Board) { - if let newFiles = filesToAdd { - board.add(newFiles) - } - let files = board.files.filter({ $0.identifier == nil }) - let parameters = AddFilesParameters(with: files) - - guard let identifier = board.identifier else { - finish(with: .failure(WeTransfer.Error.transferNotYetCreated)) - return - } - - WeTransfer.request(.addFiles(boardIdentifier: identifier), parameters: parameters.files) { [weak self] result in - switch result { - case .success(let responseFiles): - - var responseFilePool = Array(responseFiles) - - let updatedFiles: [File] = files.compactMap({ file in - guard let responseFileIndex = responseFilePool.firstIndex(where: { $0.name == file.filename && $0.size == file.filesize }) else { - return nil - } - let responseFile = responseFilePool.remove(at: responseFileIndex) - file.update(with: responseFile.id, - numberOfChunks: responseFile.multipart.partNumbers, - chunkSize: responseFile.multipart.chunkSize, - multipartUploadIdentifier: responseFile.multipart.id) - return file - }) - - guard updatedFiles.count == files.count else { - self?.finish(with: .failure(Error.incompleteFileDataReceived)) - return - } - self?.finish(with: .success(board)) - case .failure(let error): - self?.finish(with: .failure(error)) - } - } - } + + enum Error: Swift.Error, LocalizedError { + /// Not all files or incorrect file data returned by server + case incompleteFileDataReceived + + var localizedDescription: String { + switch self { + case .incompleteFileDataReceived: + return "Server did not create the correct files" + } + } + } + + /// The files to be added to the transfer if added during the initialization + private var filesToAdd: [File]? + + /// Initializes the operation with a transfer object and array of files to add. When initalized as part of a chain after `CreateTransferOperation`, this operation can be initialized without any arguments + /// + /// - Parameters: + /// - transfer: Transfer object to add the files to + /// - files: Files to be added to the transfer + convenience init(board: Board, files: [File]) { + self.init(input: board) + filesToAdd = files + } + + override func execute(_ board: Board) { + if let newFiles = filesToAdd { + board.add(newFiles) + } + let files = board.files.filter({ $0.identifier == nil }) + let parameters = AddFilesParameters(with: files) + + guard let identifier = board.identifier else { + finish(with: .failure(WeTransfer.Error.transferNotYetCreated)) + return + } + + WeTransfer.request(.addFiles(boardIdentifier: identifier), parameters: parameters.files) { [weak self] result in + switch result { + case .success(let responseFiles): + + var responseFilePool = Array(responseFiles) + + let updatedFiles: [File] = files.compactMap({ file in + guard let responseFileIndex = responseFilePool.firstIndex(where: { $0.name == file.filename && $0.size == file.filesize }) else { + return nil + } + let responseFile = responseFilePool.remove(at: responseFileIndex) + file.update(with: responseFile.id, + numberOfChunks: responseFile.multipart.partNumbers, + chunkSize: responseFile.multipart.chunkSize, + multipartUploadIdentifier: responseFile.multipart.id) + return file + }) + + guard updatedFiles.count == files.count else { + self?.finish(with: .failure(Error.incompleteFileDataReceived)) + return + } + self?.finish(with: .success(board)) + case .failure(let error): + self?.finish(with: .failure(error)) + } + } + } } diff --git a/WeTransfer/Server/Operations/CompleteUploadOperation.swift b/WeTransfer/Server/Operations/CompleteUploadOperation.swift index eb98bfb..5ea2101 100644 --- a/WeTransfer/Server/Operations/CompleteUploadOperation.swift +++ b/WeTransfer/Server/Operations/CompleteUploadOperation.swift @@ -10,93 +10,93 @@ import Foundation /// Completes the upload of each file in a transfer. Typically used in `UploadFileOperation` after all the file's chunks have been uploaded final class CompleteUploadOperation: AsynchronousResultOperation { - - enum Error: Swift.Error, LocalizedError { - /// File has not been added to the transfer yet - case fileNotYetAdded - - var localizedDescription: String { - switch self { - case .fileNotYetAdded: - return "File has not been added to the transfer yet" - } - } - } - - /// File to complete the uploading of - private let file: File - - /// Transfer or Board containing file - private let container: Transferable - - /// Initializes the operation with a file to complete the upload for - /// - /// - Parameters: - /// - container: Transferable object containing the file - /// - file: File struct for which to complete the upload for - required init(container: Transferable, file: File) { - self.container = container - self.file = file - super.init() - } - - override func execute() { - - guard let containerIdentifier = container.identifier, - let fileIdentifier = file.identifier, - let numberOfChunks = file.numberOfChunks else { - finish(with: .failure(Error.fileNotYetAdded)) - return - } - - let resultDependencies = dependencies.compactMap({ $0 as? AsynchronousResultOperation }) - let errors = resultDependencies.compactMap({ $0.result?.error }) - - if let error = errors.last { - finish(with: .failure(error)) - return - } - - if container is Transfer { - performTransferRequest(transferIdentifier: containerIdentifier, fileIdentifier: fileIdentifier, numberOfChunks: numberOfChunks) - } else if container is Board { - performBoardRequest(boardIdentifier: containerIdentifier, fileIdentifier: fileIdentifier) - } else { - fatalError("Container type '\(type(of: container))' is not supported") - } - } - - private func performTransferRequest(transferIdentifier: String, fileIdentifier: String, numberOfChunks: Int) { - let request: APIEndpoint = .completeTransferFileUpload(transferIdentifier: transferIdentifier, fileIdentifier: fileIdentifier) - let parameters = CompleteTransferFileUploadParameters(partNumbers: numberOfChunks) - WeTransfer.request(request, parameters: parameters) { [weak self] result in - guard let self = self else { - return - } - switch result { - case .failure(let error): - self.finish(with: .failure(error)) - case .success: - self.finish(with: .success(self.file)) - } - } - } - - private func performBoardRequest(boardIdentifier: String, fileIdentifier: String) { - WeTransfer.request(.completeBoardFileUpload(boardIdentifier: boardIdentifier, fileIdentifier: fileIdentifier)) { [weak self] result in - guard let self = self else { - return - } - switch result { - case .failure(let error): - self.finish(with: .failure(error)) - case .success(let response): - guard response.success else { - self.finish(with: .failure(WeTransfer.RequestError.serverError(errorMessage: response.message, httpCode: nil))) - return - } - self.finish(with: .success(self.file)) - } - } - } + + enum Error: Swift.Error, LocalizedError { + /// File has not been added to the transfer yet + case fileNotYetAdded + + var localizedDescription: String { + switch self { + case .fileNotYetAdded: + return "File has not been added to the transfer yet" + } + } + } + + /// File to complete the uploading of + private let file: File + + /// Transfer or Board containing file + private let container: Transferable + + /// Initializes the operation with a file to complete the upload for + /// + /// - Parameters: + /// - container: Transferable object containing the file + /// - file: File struct for which to complete the upload for + required init(container: Transferable, file: File) { + self.container = container + self.file = file + super.init() + } + + override func execute() { + + guard let containerIdentifier = container.identifier, + let fileIdentifier = file.identifier, + let numberOfChunks = file.numberOfChunks else { + finish(with: .failure(Error.fileNotYetAdded)) + return + } + + let resultDependencies = dependencies.compactMap({ $0 as? AsynchronousResultOperation }) + let errors = resultDependencies.compactMap({ $0.result?.error }) + + if let error = errors.last { + finish(with: .failure(error)) + return + } + + if container is Transfer { + performTransferRequest(transferIdentifier: containerIdentifier, fileIdentifier: fileIdentifier, numberOfChunks: numberOfChunks) + } else if container is Board { + performBoardRequest(boardIdentifier: containerIdentifier, fileIdentifier: fileIdentifier) + } else { + fatalError("Container type '\(type(of: container))' is not supported") + } + } + + private func performTransferRequest(transferIdentifier: String, fileIdentifier: String, numberOfChunks: Int) { + let request: APIEndpoint = .completeTransferFileUpload(transferIdentifier: transferIdentifier, fileIdentifier: fileIdentifier) + let parameters = CompleteTransferFileUploadParameters(partNumbers: numberOfChunks) + WeTransfer.request(request, parameters: parameters) { [weak self] result in + guard let self = self else { + return + } + switch result { + case .failure(let error): + self.finish(with: .failure(error)) + case .success: + self.finish(with: .success(self.file)) + } + } + } + + private func performBoardRequest(boardIdentifier: String, fileIdentifier: String) { + WeTransfer.request(.completeBoardFileUpload(boardIdentifier: boardIdentifier, fileIdentifier: fileIdentifier)) { [weak self] result in + guard let self = self else { + return + } + switch result { + case .failure(let error): + self.finish(with: .failure(error)) + case .success(let response): + guard response.success else { + self.finish(with: .failure(WeTransfer.RequestError.serverError(errorMessage: response.message, httpCode: nil))) + return + } + self.finish(with: .success(self.file)) + } + } + } } diff --git a/WeTransfer/Server/Operations/CreateBoardOperation.swift b/WeTransfer/Server/Operations/CreateBoardOperation.swift index 4f8fe22..065d2e8 100644 --- a/WeTransfer/Server/Operations/CreateBoardOperation.swift +++ b/WeTransfer/Server/Operations/CreateBoardOperation.swift @@ -11,44 +11,44 @@ import Foundation /// Operation responsible for creating the transfer on the server and providing the given transfer object with an identifier and URL when succeeded. /// This operation does not handle the requests necessary to add files to the server side transfer, which `AddFilesOperation` is responsible for final class CreateBoardOperation: AsynchronousResultOperation { - - private let board: Board - - /// Initializes the operation with the necessary properties for a new board - /// - /// - Parameters: - /// - name: Name of the board to be created - /// - description: Optional description of the board to be created - convenience init(name: String, description: String?) { - self.init(board: Board(name: name, description: description)) - } - - /// Initalizes the operation with a board to be created on the server - /// - /// - Parameter board: Board object - required init(board: Board) { - self.board = board - super.init() - } - - override func execute() { - guard board.identifier == nil else { - finish(with: .success(board)) - return - } - - let parameters = CreateBoardParameters(with: board) - WeTransfer.request(.createBoard(), parameters: parameters) { [weak self] result in - guard let self = self else { - return - } - switch result { - case .success(let response): - self.board.update(with: response.id, shortURL: response.url) - self.finish(with: .success(self.board)) - case .failure(let error): - self.finish(with: .failure(error)) - } - } - } + + private let board: Board + + /// Initializes the operation with the necessary properties for a new board + /// + /// - Parameters: + /// - name: Name of the board to be created + /// - description: Optional description of the board to be created + convenience init(name: String, description: String?) { + self.init(board: Board(name: name, description: description)) + } + + /// Initalizes the operation with a board to be created on the server + /// + /// - Parameter board: Board object + required init(board: Board) { + self.board = board + super.init() + } + + override func execute() { + guard board.identifier == nil else { + finish(with: .success(board)) + return + } + + let parameters = CreateBoardParameters(with: board) + WeTransfer.request(.createBoard(), parameters: parameters) { [weak self] result in + guard let self = self else { + return + } + switch result { + case .success(let response): + self.board.update(with: response.id, shortURL: response.url) + self.finish(with: .success(self.board)) + case .failure(let error): + self.finish(with: .failure(error)) + } + } + } } diff --git a/WeTransfer/Server/Operations/CreateChunkOperation.swift b/WeTransfer/Server/Operations/CreateChunkOperation.swift index b864c54..0e93f40 100644 --- a/WeTransfer/Server/Operations/CreateChunkOperation.swift +++ b/WeTransfer/Server/Operations/CreateChunkOperation.swift @@ -10,69 +10,69 @@ import Foundation /// Creates a chunk of a file to be uploaded. Designed to be used right before `UploadChunkOperation` final class CreateChunkOperation: AsynchronousResultOperation { - - enum Error: Swift.Error, LocalizedError { - /// File has not been added to the transfer yet - case fileNotYetAdded - - var localizedDescription: String { - switch self { - case .fileNotYetAdded: - return "File has not been added to the transfer yet" - } - } - } - - private let container: Transferable - - /// File to create chunk from - private let file: File - /// Index of chunk from file - private let chunkIndex: Int - - /// Initalizes the operation with a file and an index of the chunk - /// - /// - Parameters: - /// - file: File struct of the file to create the chunk from - /// - chunkIndex: Index of the chunk to be created - required init(container: Transferable, file: File, chunkIndex: Int) { - self.container = container - self.file = file - self.chunkIndex = chunkIndex - } - - override func execute() { - guard let containerIdentifier = container.identifier, let fileIdentifier = file.identifier else { - finish(with: .failure(Error.fileNotYetAdded)) - return - } - - let endpoint: APIEndpoint - if container is Transfer { - endpoint = .requestTransferUploadURL(transferIdentifier: containerIdentifier, fileIdentifier: fileIdentifier, chunkIndex: chunkIndex) - } else if container is Board { - guard let multipartIdentifier = file.multipartUploadIdentifier else { - finish(with: .failure(Error.fileNotYetAdded)) - return - } - endpoint = .requestBoardUploadURL(boardIdentifier: containerIdentifier, fileIdentifier: fileIdentifier, chunkIndex: chunkIndex, multipartIdentifier: multipartIdentifier) - } else { - fatalError("Container type '\(type(of: container))' is not supported") - } - - WeTransfer.request(endpoint) { [weak self] result in - switch result { - case .failure(let error): - self?.finish(with: .failure(error)) - case .success(let response): - guard let self = self else { - return - } - // Chunks are locally referenced in a zero-based index. Subtract 1 from partNumber value - let chunk = Chunk(file: self.file, chunkIndex: self.chunkIndex, uploadURL: response.url) - self.finish(with: .success(chunk)) - } - } - } - + + enum Error: Swift.Error, LocalizedError { + /// File has not been added to the transfer yet + case fileNotYetAdded + + var localizedDescription: String { + switch self { + case .fileNotYetAdded: + return "File has not been added to the transfer yet" + } + } + } + + private let container: Transferable + + /// File to create chunk from + private let file: File + /// Index of chunk from file + private let chunkIndex: Int + + /// Initalizes the operation with a file and an index of the chunk + /// + /// - Parameters: + /// - file: File struct of the file to create the chunk from + /// - chunkIndex: Index of the chunk to be created + required init(container: Transferable, file: File, chunkIndex: Int) { + self.container = container + self.file = file + self.chunkIndex = chunkIndex + } + + override func execute() { + guard let containerIdentifier = container.identifier, let fileIdentifier = file.identifier else { + finish(with: .failure(Error.fileNotYetAdded)) + return + } + + let endpoint: APIEndpoint + if container is Transfer { + endpoint = .requestTransferUploadURL(transferIdentifier: containerIdentifier, fileIdentifier: fileIdentifier, chunkIndex: chunkIndex) + } else if container is Board { + guard let multipartIdentifier = file.multipartUploadIdentifier else { + finish(with: .failure(Error.fileNotYetAdded)) + return + } + endpoint = .requestBoardUploadURL(boardIdentifier: containerIdentifier, fileIdentifier: fileIdentifier, chunkIndex: chunkIndex, multipartIdentifier: multipartIdentifier) + } else { + fatalError("Container type '\(type(of: container))' is not supported") + } + + WeTransfer.request(endpoint) { [weak self] result in + switch result { + case .failure(let error): + self?.finish(with: .failure(error)) + case .success(let response): + guard let self = self else { + return + } + // Chunks are locally referenced in a zero-based index. Subtract 1 from partNumber value + let chunk = Chunk(file: self.file, chunkIndex: self.chunkIndex, uploadURL: response.url) + self.finish(with: .success(chunk)) + } + } + } + } diff --git a/WeTransfer/Server/Operations/CreateTransferOperation.swift b/WeTransfer/Server/Operations/CreateTransferOperation.swift index 524b132..ca0b864 100644 --- a/WeTransfer/Server/Operations/CreateTransferOperation.swift +++ b/WeTransfer/Server/Operations/CreateTransferOperation.swift @@ -11,71 +11,71 @@ import Foundation /// Operation responsible for creating the transfer on the server and providing the given transfer object with an identifier and URL when succeeded. /// This operation does not handle the requests necessary to add files to the server side transfer, which `AddFilesOperation` is responsible for final class CreateTransferOperation: AsynchronousResultOperation { - - enum Error: Swift.Error, LocalizedError { - /// Not all files or incorrect file data returned by server - case incompleteFileDataReceived - - var localizedDescription: String { - switch self { - case .incompleteFileDataReceived: - return "Server did not create the correct files" - } - } - } - - let message: String - let fileURLs: [URL] - - /// Initalized the operation with the necessary parameters for a transfer - /// - /// - Parameter transfer: Transfer object with optionally some files already added - required init(message: String, fileURLs: [URL]) { - self.message = message - self.fileURLs = fileURLs - super.init() - } - - override func execute() { - let files: [File] - do { - files = try fileURLs.map({ try File(url: $0) }) - } catch { - // Fail when any of the files failed to create - finish(with: .failure(error)) - return - } - - let parameters = CreateTransferParameters(message: message, files: files) - WeTransfer.request(.createTransfer(), parameters: parameters) { [weak self] result in - guard let self = self else { - return - } - switch result { - case .success(let response): - var responseFiles = response.files - - let updatedFiles: [File] = files.compactMap({ file in - guard let responseFileIndex = responseFiles.firstIndex(where: { $0.name == file.filename && $0.size == file.filesize }) else { - return nil - } - let responseFile = responseFiles.remove(at: responseFileIndex) - file.update(with: responseFile.identifier, - numberOfChunks: responseFile.multipartUploadInfo.partNumbers, - chunkSize: responseFile.multipartUploadInfo.chunkSize, - multipartUploadIdentifier: nil) - return file - }) - - guard updatedFiles.count == files.count else { - self.finish(with: .failure(Error.incompleteFileDataReceived)) - return - } - let transfer = Transfer(identifier: response.id, message: parameters.message, files: updatedFiles) - self.finish(with: .success(transfer)) - case .failure(let error): - self.finish(with: .failure(error)) - } - } - } + + enum Error: Swift.Error, LocalizedError { + /// Not all files or incorrect file data returned by server + case incompleteFileDataReceived + + var localizedDescription: String { + switch self { + case .incompleteFileDataReceived: + return "Server did not create the correct files" + } + } + } + + let message: String + let fileURLs: [URL] + + /// Initalized the operation with the necessary parameters for a transfer + /// + /// - Parameter transfer: Transfer object with optionally some files already added + required init(message: String, fileURLs: [URL]) { + self.message = message + self.fileURLs = fileURLs + super.init() + } + + override func execute() { + let files: [File] + do { + files = try fileURLs.map({ try File(url: $0) }) + } catch { + // Fail when any of the files failed to create + finish(with: .failure(error)) + return + } + + let parameters = CreateTransferParameters(message: message, files: files) + WeTransfer.request(.createTransfer(), parameters: parameters) { [weak self] result in + guard let self = self else { + return + } + switch result { + case .success(let response): + var responseFiles = response.files + + let updatedFiles: [File] = files.compactMap({ file in + guard let responseFileIndex = responseFiles.firstIndex(where: { $0.name == file.filename && $0.size == file.filesize }) else { + return nil + } + let responseFile = responseFiles.remove(at: responseFileIndex) + file.update(with: responseFile.identifier, + numberOfChunks: responseFile.multipartUploadInfo.partNumbers, + chunkSize: responseFile.multipartUploadInfo.chunkSize, + multipartUploadIdentifier: nil) + return file + }) + + guard updatedFiles.count == files.count else { + self.finish(with: .failure(Error.incompleteFileDataReceived)) + return + } + let transfer = Transfer(identifier: response.id, message: parameters.message, files: updatedFiles) + self.finish(with: .success(transfer)) + case .failure(let error): + self.finish(with: .failure(error)) + } + } + } } diff --git a/WeTransfer/Server/Operations/FinalizeTransferOperation.swift b/WeTransfer/Server/Operations/FinalizeTransferOperation.swift index 5a65f02..62626e2 100644 --- a/WeTransfer/Server/Operations/FinalizeTransferOperation.swift +++ b/WeTransfer/Server/Operations/FinalizeTransferOperation.swift @@ -10,34 +10,34 @@ import Foundation /// Finalizes the provided Transfer after all files have been uploaded. The Transfer object will be updated with a URL as a result. final class FinalizeTransferOperation: ChainedAsynchronousResultOperation { - - /// Initializes the operation with a transfer. When initalized as part of a chain this operation can be initialized without any arguments - /// - /// - Parameter container: Transfer object to finalize on the server - convenience init(container: Transfer) { - self.init(input: container) - } - - override func execute(_ transfer: Transfer) { - guard transfer.shortURL == nil else { - finish(with: .failure(WeTransfer.Error.transferAlreadyFinalized)) - return - } - - guard let identifier = transfer.identifier else { - finish(with: .failure(WeTransfer.Error.transferNotYetCreated)) - return - } - - WeTransfer.request(.finalizeTransfer(transferIdentifier: identifier)) { [weak self] result in - switch result { - case .success(let response): - transfer.update(with: response.url) - self?.finish(with: .success(transfer)) - case .failure(let error): - self?.finish(with: .failure(error)) - } - } - } - + + /// Initializes the operation with a transfer. When initalized as part of a chain this operation can be initialized without any arguments + /// + /// - Parameter container: Transfer object to finalize on the server + convenience init(container: Transfer) { + self.init(input: container) + } + + override func execute(_ transfer: Transfer) { + guard transfer.shortURL == nil else { + finish(with: .failure(WeTransfer.Error.transferAlreadyFinalized)) + return + } + + guard let identifier = transfer.identifier else { + finish(with: .failure(WeTransfer.Error.transferNotYetCreated)) + return + } + + WeTransfer.request(.finalizeTransfer(transferIdentifier: identifier)) { [weak self] result in + switch result { + case .success(let response): + transfer.update(with: response.url) + self?.finish(with: .success(transfer)) + case .failure(let error): + self?.finish(with: .failure(error)) + } + } + } + } diff --git a/WeTransfer/Server/Operations/Helpers/Result.swift b/WeTransfer/Server/Operations/Helpers/Result.swift index 14ccdd9..61ef767 100644 --- a/WeTransfer/Server/Operations/Helpers/Result.swift +++ b/WeTransfer/Server/Operations/Helpers/Result.swift @@ -10,18 +10,18 @@ import Foundation /// Enum with either a value when succeeded or an error when failed public enum Result { - /// The operation has succeeded and requested value is available - case success(Value) - /// The operation has failed with the provided error - case failure(Error) - - public var error: Error? { - guard case .failure(let error) = self else { return nil } - return error - } - - public var value: Value? { - guard case .success(let value) = self else { return nil } - return value - } + /// The operation has succeeded and requested value is available + case success(Value) + /// The operation has failed with the provided error + case failure(Error) + + public var error: Error? { + guard case .failure(let error) = self else { return nil } + return error + } + + public var value: Value? { + guard case .success(let value) = self else { return nil } + return value + } } diff --git a/WeTransfer/Server/Operations/UploadChunkOperation.swift b/WeTransfer/Server/Operations/UploadChunkOperation.swift index 411461f..c4810dc 100644 --- a/WeTransfer/Server/Operations/UploadChunkOperation.swift +++ b/WeTransfer/Server/Operations/UploadChunkOperation.swift @@ -10,53 +10,53 @@ import Foundation /// Uploads the chunk that resulted from the dependant `CreateChunkOperation`. The uploading is handled by the provided `URLSession` final class UploadChunkOperation: ChainedAsynchronousResultOperation { - - enum Error: Swift.Error, LocalizedError { - /// No chunk data available. File at URL might be inaccessible - case noChunkDataAvailable - /// Upload did not succeed - case uploadFailed - - var localizedDescription: String { - switch self { - case .noChunkDataAvailable: - return "No chunk data available. File at URL might be inaccessible" - case .uploadFailed: - return "Chunk Upload did not succeed" - } - } - } - - /// URLSession handling the creation and actual uploading of the chunk - let session: URLSession - - /// Initializes the operation with a session which handles the actual uploading part - /// - /// - Parameter session: `URLSession` that should create and be responsible of the actual uploading part - required init(session: URLSession) { - self.session = session - super.init() - } - - override func execute(_ chunk: Chunk) { - guard let data = try? Data(from: chunk) else { - finish(with: .failure(Error.noChunkDataAvailable)) - return - } - - var urlRequest = URLRequest(url: chunk.uploadURL) - urlRequest.httpMethod = "PUT" - let task = self.session.uploadTask(with: urlRequest, from: data) { [weak self] (_, urlResponse, error) in - if let error = error { - self?.finish(with: .failure(error)) - return - } - if let response = urlResponse as? HTTPURLResponse, !(200...299).contains(response.statusCode) { - self?.finish(with: .failure(Error.uploadFailed)) - return - } - self?.finish(with: .success(chunk)) - } - task.resume() - } + + enum Error: Swift.Error, LocalizedError { + /// No chunk data available. File at URL might be inaccessible + case noChunkDataAvailable + /// Upload did not succeed + case uploadFailed + + var localizedDescription: String { + switch self { + case .noChunkDataAvailable: + return "No chunk data available. File at URL might be inaccessible" + case .uploadFailed: + return "Chunk Upload did not succeed" + } + } + } + + /// URLSession handling the creation and actual uploading of the chunk + let session: URLSession + + /// Initializes the operation with a session which handles the actual uploading part + /// + /// - Parameter session: `URLSession` that should create and be responsible of the actual uploading part + required init(session: URLSession) { + self.session = session + super.init() + } + + override func execute(_ chunk: Chunk) { + guard let data = try? Data(from: chunk) else { + finish(with: .failure(Error.noChunkDataAvailable)) + return + } + + var urlRequest = URLRequest(url: chunk.uploadURL) + urlRequest.httpMethod = "PUT" + let task = self.session.uploadTask(with: urlRequest, from: data) { [weak self] (_, urlResponse, error) in + if let error = error { + self?.finish(with: .failure(error)) + return + } + if let response = urlResponse as? HTTPURLResponse, !(200...299).contains(response.statusCode) { + self?.finish(with: .failure(Error.uploadFailed)) + return + } + self?.finish(with: .success(chunk)) + } + task.resume() + } } diff --git a/WeTransfer/Server/Operations/UploadFileOperation.swift b/WeTransfer/Server/Operations/UploadFileOperation.swift index 4ab95b0..aba91e1 100644 --- a/WeTransfer/Server/Operations/UploadFileOperation.swift +++ b/WeTransfer/Server/Operations/UploadFileOperation.swift @@ -10,75 +10,75 @@ import Foundation /// Responsible for creating the necessary operations to create and upload chunks for the provided file. Uses the provided operation queue to handle created operations and the actual uploading is done with the provided URLSession final class UploadFileOperation: AsynchronousResultOperation { - - enum Error: Swift.Error, LocalizedError { - /// File has no chunks to upload - case noChunksAvailable - - var localizedDescription: String { - switch self { - case .noChunksAvailable: - return "File has no chunks to upload" - } - } - } - - /// Transfer or Board containing file - private let container: Transferable - /// File to upload - private let file: File - /// Queue to add the created operations to - private let operationQueue: OperationQueue - /// URLSession handling the creation and actual uploading of the chunks - private let session: URLSession - - /// Initializes the operation with the necessary file, operation queue and session - /// - /// - Parameters: - /// - file: The file from which to create and upload the chunks - /// - operationQueue: Operation queue to add the operations to - /// - session: URLSession that should handle the actual uploading - required init(container: Transferable, file: File, operationQueue: OperationQueue, session: URLSession) { - self.container = container - self.file = file - self.operationQueue = operationQueue - self.session = session - super.init() - } - - /// Creates the necessary operations for each chunk to be created and uploaded, with the create operation being chained to the upload operation so they happen subsequently. All upload operations are then added as a dependency to the provided complete operation. - /// - /// - Parameter completeOperation: Operation depending on all upload operations. Should only be executed when all chunk operations have finished - /// - Returns: An array of chunk operations, whith the each pair of create and upload operations being chained - private func chainedChunkOperations(with completeOperation: CompleteUploadOperation) -> [Operation] { - guard let numberOfChunks = file.numberOfChunks else { - return [] - } - var operations = [Operation]() - for chunkIndex in 0.. [Operation] { + guard let numberOfChunks = file.numberOfChunks else { + return [] + } + var operations = [Operation]() + for chunkIndex in 0..: ChainedAsynchronousResultOperation, URLSessionDataDelegate { - - /// Amount of bytes already sent - private var bytesSent: Bytes = 0 - /// The total amount of bytes to be sent - private var totalBytes: Bytes = 0 - - /// The total progress for all files to be uploaded - let progress = Progress(totalUnitCount: 1) - - /// Initializes the operation with a transfer or a board. When initalized as part of a chain after `AddFilesOperation`, this operation can be initialized without any arguments - /// - /// - Parameter container: Transfer or Board object to upload files from - convenience init(container: Container) { - self.init(input: container) - } - - override func execute(_ container: Container) { - - let files = container.files.filter({ $0.isUploaded == false }) - - guard !files.isEmpty else { - finish(with: .failure(WeTransfer.Error.noFilesAvailable)) - return - } - - totalBytes = files.reduce(0, { $0 + $1.filesize }) - - // Each seperate files are handled in a queue - let fileOperationQueue = OperationQueue() - - // OperationQueue that handles all chunks concurrently - let chunkOperationQueue = OperationQueue() - chunkOperationQueue.maxConcurrentOperationCount = 5 - - // Seperate URLSession that handles the actual uploading and reports the upload progress - let uploadSession = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil) - progress.totalUnitCount = Int64(self.totalBytes) - - // Use the queue of the uploadSession to handle the progress - uploadSession.delegateQueue.underlyingQueue?.async { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.progress.becomeCurrent(withPendingUnitCount: Int64(strongSelf.totalBytes)) - } - - let filesResultOperation = AsynchronousDependencyResultOperation() - - let fileOperations = files.compactMap { file -> UploadFileOperation? in - guard file.identifier != nil else { - // File may have been added while operations have created, fail silently - return nil - } - let operation = UploadFileOperation(container: container, file: file, operationQueue: chunkOperationQueue, session: uploadSession) - operation.onResult = { result in - if case .success(let file) = result { - file.isUploaded = true - } - } - filesResultOperation.addDependency(operation) - return operation - } - - guard !fileOperations.isEmpty else { - // No files to upload, fail - finish(with: .failure(WeTransfer.Error.noFilesAvailable)) - return - } - - filesResultOperation.onResult = { [weak self] result in - uploadSession.delegateQueue.underlyingQueue?.async { - self?.progress.resignCurrent() - } - switch result { - case .success: - self?.finish(with: .success(container)) - case .failure(let error): - self?.finish(with: .failure(error)) - } - } - - fileOperationQueue.addOperations(fileOperations + [filesResultOperation], waitUntilFinished: false) - } - - // Use the didSendBodyData delegate method to update the upload progress - func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { - self.bytesSent += UInt64(bytesSent) - progress.completedUnitCount = Int64(self.bytesSent) - } + + /// Amount of bytes already sent + private var bytesSent: Bytes = 0 + /// The total amount of bytes to be sent + private var totalBytes: Bytes = 0 + + /// The total progress for all files to be uploaded + let progress = Progress(totalUnitCount: 1) + + /// Initializes the operation with a transfer or a board. When initalized as part of a chain after `AddFilesOperation`, this operation can be initialized without any arguments + /// + /// - Parameter container: Transfer or Board object to upload files from + convenience init(container: Container) { + self.init(input: container) + } + + override func execute(_ container: Container) { + + let files = container.files.filter({ $0.isUploaded == false }) + + guard !files.isEmpty else { + finish(with: .failure(WeTransfer.Error.noFilesAvailable)) + return + } + + totalBytes = files.reduce(0, { $0 + $1.filesize }) + + // Each seperate files are handled in a queue + let fileOperationQueue = OperationQueue() + + // OperationQueue that handles all chunks concurrently + let chunkOperationQueue = OperationQueue() + chunkOperationQueue.maxConcurrentOperationCount = 5 + + // Seperate URLSession that handles the actual uploading and reports the upload progress + let uploadSession = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil) + progress.totalUnitCount = Int64(self.totalBytes) + + // Use the queue of the uploadSession to handle the progress + uploadSession.delegateQueue.underlyingQueue?.async { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.progress.becomeCurrent(withPendingUnitCount: Int64(strongSelf.totalBytes)) + } + + let filesResultOperation = AsynchronousDependencyResultOperation() + + let fileOperations = files.compactMap { file -> UploadFileOperation? in + guard file.identifier != nil else { + // File may have been added while operations have created, fail silently + return nil + } + let operation = UploadFileOperation(container: container, file: file, operationQueue: chunkOperationQueue, session: uploadSession) + operation.onResult = { result in + if case .success(let file) = result { + file.isUploaded = true + } + } + filesResultOperation.addDependency(operation) + return operation + } + + guard !fileOperations.isEmpty else { + // No files to upload, fail + finish(with: .failure(WeTransfer.Error.noFilesAvailable)) + return + } + + filesResultOperation.onResult = { [weak self] result in + uploadSession.delegateQueue.underlyingQueue?.async { + self?.progress.resignCurrent() + } + switch result { + case .success: + self?.finish(with: .success(container)) + case .failure(let error): + self?.finish(with: .failure(error)) + } + } + + fileOperationQueue.addOperations(fileOperations + [filesResultOperation], waitUntilFinished: false) + } + + // Use the didSendBodyData delegate method to update the upload progress + func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + self.bytesSent += UInt64(bytesSent) + progress.completedUnitCount = Int64(self.bytesSent) + } } diff --git a/WeTransfer/WeTransfer.swift b/WeTransfer/WeTransfer.swift index bc2ea42..f8e9ae6 100644 --- a/WeTransfer/WeTransfer.swift +++ b/WeTransfer/WeTransfer.swift @@ -13,186 +13,186 @@ import Foundation /// - Use `WeTransfer.sendTransfer()` to send a transfer right away /// - Use `WeTransfer.createTransfer()` to manually create a transfer on the server and `WeTransfer.send()` to upload the transfer when you're ready public struct WeTransfer { - - /// The client used for all requests. Stores the authenticated state and creates and manages all requests - static var client: APIClient = APIClient() - - private init() {} + + /// The client used for all requests. Stores the authenticated state and creates and manages all requests + static var client: APIClient = APIClient() + + private init() {} } extension WeTransfer { - - /// Possible errors thrown from multiple points in the transfer progress - public enum Error: Swift.Error, LocalizedError { - /// WeTransfer client not configured yet, make sure to call `WeTransfer.configure(with configuration:)` - case notConfigured - /// Authorization failed when performing request - case notAuthorized - /// Transfer is already created so create transfer request should not be called again - case transferAlreadyCreated - /// Transfer is not yet created so other request regarding the transfer will fail - case transferNotYetCreated - /// Transfer has no files to share as no files are added yet or all files are already uploaded - case noFilesAvailable - /// Transfer already finalized, not need to call finalize again - case transferAlreadyFinalized - - public var errorDescription: String? { - switch self { - case .notConfigured: - return "Framework should configured with at least an API key" - case .notAuthorized: - return "Not authorized: invalid API key used for request" - case .transferAlreadyCreated: - return "Transfer already created: create transfer request should not be called multiple times for the same transfer" - case .noFilesAvailable: - return "No files available or all files have already been uploaded: add files to the transfer to upload" - case .transferAlreadyFinalized: - return "Transfer already finalized" - default: - return "\(self)" - } - } - } - - /// Configuration of the API client - public struct Configuration { - public let apiKey: String - public let baseURL: URL - - /// Initializes the configuration struct with an API key and optionally a baseURL for when you're pointing to a different server - /// - /// - Parameters: - /// - APIKey: Key required to make use of the API. Visit https://developers.wetransfer.com to get a key - /// - baseURL: Defaults to the standard API, but can be used to point to a different server - public init(apiKey: String, baseURL: URL? = nil) { - // swiftlint:disable:next force_unwrapping - self.baseURL = baseURL ?? URL(string: "https://dev.wetransfer.com/v2/")! - self.apiKey = apiKey - } - } - - /// Configures the API client with the provided configuration - /// - /// - Parameter configuration: Configuration struct to configure the API client with - public static func configure(with configuration: Configuration) { - client.apiKey = configuration.apiKey - client.baseURL = configuration.baseURL - } + + /// Possible errors thrown from multiple points in the transfer progress + public enum Error: Swift.Error, LocalizedError { + /// WeTransfer client not configured yet, make sure to call `WeTransfer.configure(with configuration:)` + case notConfigured + /// Authorization failed when performing request + case notAuthorized + /// Transfer is already created so create transfer request should not be called again + case transferAlreadyCreated + /// Transfer is not yet created so other request regarding the transfer will fail + case transferNotYetCreated + /// Transfer has no files to share as no files are added yet or all files are already uploaded + case noFilesAvailable + /// Transfer already finalized, not need to call finalize again + case transferAlreadyFinalized + + public var errorDescription: String? { + switch self { + case .notConfigured: + return "Framework should configured with at least an API key" + case .notAuthorized: + return "Not authorized: invalid API key used for request" + case .transferAlreadyCreated: + return "Transfer already created: create transfer request should not be called multiple times for the same transfer" + case .noFilesAvailable: + return "No files available or all files have already been uploaded: add files to the transfer to upload" + case .transferAlreadyFinalized: + return "Transfer already finalized" + default: + return "\(self)" + } + } + } + + /// Configuration of the API client + public struct Configuration { + public let apiKey: String + public let baseURL: URL + + /// Initializes the configuration struct with an API key and optionally a baseURL for when you're pointing to a different server + /// + /// - Parameters: + /// - APIKey: Key required to make use of the API. Visit https://developers.wetransfer.com to get a key + /// - baseURL: Defaults to the standard API, but can be used to point to a different server + public init(apiKey: String, baseURL: URL? = nil) { + // swiftlint:disable:next force_unwrapping + self.baseURL = baseURL ?? URL(string: "https://dev.wetransfer.com/v2/")! + self.apiKey = apiKey + } + } + + /// Configures the API client with the provided configuration + /// + /// - Parameter configuration: Configuration struct to configure the API client with + public static func configure(with configuration: Configuration) { + client.apiKey = configuration.apiKey + client.baseURL = configuration.baseURL + } } extension WeTransfer { - - /// Immediately uploads files to a transfer with the provided name and file URLs - /// - /// - Parameters: - /// - name: Name of the transfer, shown when user opens the resulting link - /// - fileURLS: Array of URLs pointing to files to be added to the transfer - /// - stateChanged: Closure that will be called for state updates. - /// - state: Enum describing the current transfer's state. See the `State` enum description for more details for each state - public static func uploadTransfer(saying message: String, containing fileURLS: [URL], stateChanged: @escaping (_ state: State) -> Void) { - - // Make sure stateChanges closure is called on the main thread - let changeState = { state in - DispatchQueue.main.async { - stateChanged(state) - } - } - - // Create transfer on server - let creationOperation = CreateTransferOperation(message: message, fileURLs: fileURLS) - - // Upload all files from the chunks - let uploadFilesOperation = UploadFilesOperation() - - // Handle transfer created result - creationOperation.onResult = { [weak uploadFilesOperation] result in - if case .success(let transfer) = result { - changeState(.created(transfer)) - - if let operation = uploadFilesOperation { - stateChanged(.uploading(operation.progress)) - } - } - } - - // Finalize transfer to get the url - let finalizeTransferOperation = FinalizeTransferOperation() - - // Perform all operations in a chain - let operations = [creationOperation, uploadFilesOperation, finalizeTransferOperation].chained() - client.operationQueue.addOperations(operations, waitUntilFinished: false) - - // Handle the result of the very last operation that's executed - finalizeTransferOperation.onResult = { result in - switch result { - case .failure(let error): - changeState(.failed(error)) - case .success(let transfer): - changeState(.completed(transfer)) - } - } - } - - /// Immediately uploads files to a board with the provided name and file URLs - /// - /// - Parameters: - /// - name: Name of the board, shown when user opens the resulting link - /// - description: Optional description of the board - /// - fileURLS: Array of URLs pointing to files to be added to the board - /// - stateChanged: Closure that will be called for state updates. - /// - state: Enum describing the current board's state. See the `State` enum description for more details for each state - /// - Returns: Board object used to handle the transfer process. - public static func uploadBoard(named name: String, description: String?, containing fileURLS: [URL], stateChanged: @escaping (_ state: State) -> Void) { - - // Make sure stateChanges closure is called on the main thread - let changeState = { state in - DispatchQueue.main.async { - stateChanged(state) - } - } - - // Create the board locally and on the server - let board = Board(name: name, description: description) - let createOperation = CreateBoardOperation(board: board) - createOperation.onResult = { result in - if case .success(let board) = result { - changeState(.created(board)) - } - } - - // Add files to board - let files: [File] - do { - files = try fileURLS.map({ try File(url: $0) }) - } catch { - stateChanged(.failed(error)) - return - } - let addFilesOperation = AddFilesOperation(board: board, files: files) - - // Upload all files from the chunks - let uploadFilesOperation = UploadFilesOperation() - - // Set state to uploading when uploadFilesOperation is about to begin - addFilesOperation.onResult = { [weak uploadFilesOperation] result in - if case .success = result, let operation = uploadFilesOperation { - stateChanged(.uploading(operation.progress)) - } - } - - // Handle the result of the very last operation that's executed - uploadFilesOperation.onResult = { result in - switch result { - case .failure(let error): - changeState(.failed(error)) - case .success(let transfer): - changeState(.completed(transfer)) - } - } - - // Perform all operations in a chain - let operations = [createOperation, addFilesOperation, uploadFilesOperation].chained() - client.operationQueue.addOperations(operations, waitUntilFinished: false) - } + + /// Immediately uploads files to a transfer with the provided name and file URLs + /// + /// - Parameters: + /// - name: Name of the transfer, shown when user opens the resulting link + /// - fileURLS: Array of URLs pointing to files to be added to the transfer + /// - stateChanged: Closure that will be called for state updates. + /// - state: Enum describing the current transfer's state. See the `State` enum description for more details for each state + public static func uploadTransfer(saying message: String, containing fileURLS: [URL], stateChanged: @escaping (_ state: State) -> Void) { + + // Make sure stateChanges closure is called on the main thread + let changeState = { state in + DispatchQueue.main.async { + stateChanged(state) + } + } + + // Create transfer on server + let creationOperation = CreateTransferOperation(message: message, fileURLs: fileURLS) + + // Upload all files from the chunks + let uploadFilesOperation = UploadFilesOperation() + + // Handle transfer created result + creationOperation.onResult = { [weak uploadFilesOperation] result in + if case .success(let transfer) = result { + changeState(.created(transfer)) + + if let operation = uploadFilesOperation { + stateChanged(.uploading(operation.progress)) + } + } + } + + // Finalize transfer to get the url + let finalizeTransferOperation = FinalizeTransferOperation() + + // Perform all operations in a chain + let operations = [creationOperation, uploadFilesOperation, finalizeTransferOperation].chained() + client.operationQueue.addOperations(operations, waitUntilFinished: false) + + // Handle the result of the very last operation that's executed + finalizeTransferOperation.onResult = { result in + switch result { + case .failure(let error): + changeState(.failed(error)) + case .success(let transfer): + changeState(.completed(transfer)) + } + } + } + + /// Immediately uploads files to a board with the provided name and file URLs + /// + /// - Parameters: + /// - name: Name of the board, shown when user opens the resulting link + /// - description: Optional description of the board + /// - fileURLS: Array of URLs pointing to files to be added to the board + /// - stateChanged: Closure that will be called for state updates. + /// - state: Enum describing the current board's state. See the `State` enum description for more details for each state + /// - Returns: Board object used to handle the transfer process. + public static func uploadBoard(named name: String, description: String?, containing fileURLS: [URL], stateChanged: @escaping (_ state: State) -> Void) { + + // Make sure stateChanges closure is called on the main thread + let changeState = { state in + DispatchQueue.main.async { + stateChanged(state) + } + } + + // Create the board locally and on the server + let board = Board(name: name, description: description) + let createOperation = CreateBoardOperation(board: board) + createOperation.onResult = { result in + if case .success(let board) = result { + changeState(.created(board)) + } + } + + // Add files to board + let files: [File] + do { + files = try fileURLS.map({ try File(url: $0) }) + } catch { + stateChanged(.failed(error)) + return + } + let addFilesOperation = AddFilesOperation(board: board, files: files) + + // Upload all files from the chunks + let uploadFilesOperation = UploadFilesOperation() + + // Set state to uploading when uploadFilesOperation is about to begin + addFilesOperation.onResult = { [weak uploadFilesOperation] result in + if case .success = result, let operation = uploadFilesOperation { + stateChanged(.uploading(operation.progress)) + } + } + + // Handle the result of the very last operation that's executed + uploadFilesOperation.onResult = { result in + switch result { + case .failure(let error): + changeState(.failed(error)) + case .success(let transfer): + changeState(.completed(transfer)) + } + } + + // Perform all operations in a chain + let operations = [createOperation, addFilesOperation, uploadFilesOperation].chained() + client.operationQueue.addOperations(operations, waitUntilFinished: false) + } } diff --git a/WeTransferTests/AuthorizationTests.swift b/WeTransferTests/AuthorizationTests.swift index f732066..95a10ff 100644 --- a/WeTransferTests/AuthorizationTests.swift +++ b/WeTransferTests/AuthorizationTests.swift @@ -10,39 +10,39 @@ import XCTest @testable import WeTransfer final class AuthorizationTests: BaseTestCase { - - func testUnauthorized() { - do { - _ = try WeTransfer.client.createRequest(.createTransfer()) - } catch { - XCTAssertEqual(error.localizedDescription, WeTransfer.Error.notAuthorized.localizedDescription) - } - } - - func testAuthorization() { - let authorizedExpectation = expectation(description: "Authorization should succeed") - WeTransfer.authorize { (result) in - if case .failure(let error) = result { - XCTFail("Authorization failed: \(error)") - } - authorizedExpectation.fulfill() - } - - waitForExpectations(timeout: 10) { _ in } - } - - func testWrongJWTKey() { - let authFailExpectation = expectation(description: "Request should failed") - TestConfiguration.fakeAuthorize() - let board = Board(name: "Bad Transfer", description: nil) - WeTransfer.createExternalBoard(board) { result in - if case .success = result { - XCTFail("Request did not fail (like it should)") - } - authFailExpectation.fulfill() - } - - waitForExpectations(timeout: 10) { _ in } - } - + + func testUnauthorized() { + do { + _ = try WeTransfer.client.createRequest(.createTransfer()) + } catch { + XCTAssertEqual(error.localizedDescription, WeTransfer.Error.notAuthorized.localizedDescription) + } + } + + func testAuthorization() { + let authorizedExpectation = expectation(description: "Authorization should succeed") + WeTransfer.authorize { (result) in + if case .failure(let error) = result { + XCTFail("Authorization failed: \(error)") + } + authorizedExpectation.fulfill() + } + + waitForExpectations(timeout: 10) { _ in } + } + + func testWrongJWTKey() { + let authFailExpectation = expectation(description: "Request should failed") + TestConfiguration.fakeAuthorize() + let board = Board(name: "Bad Transfer", description: nil) + WeTransfer.createExternalBoard(board) { result in + if case .success = result { + XCTFail("Request did not fail (like it should)") + } + authFailExpectation.fulfill() + } + + waitForExpectations(timeout: 10) { _ in } + } + } diff --git a/WeTransferTests/BaseTestCase.swift b/WeTransferTests/BaseTestCase.swift index f548483..323ba97 100644 --- a/WeTransferTests/BaseTestCase.swift +++ b/WeTransferTests/BaseTestCase.swift @@ -10,15 +10,15 @@ import XCTest @testable import WeTransfer open class BaseTestCase: XCTestCase { - + override open func setUp() { - super.setUp() - TestConfiguration.configure(environment: .production) + super.setUp() + TestConfiguration.configure(environment: .production) } - + override open func tearDown() { - super.tearDown() - TestConfiguration.resetConfiguration() + super.tearDown() + TestConfiguration.resetConfiguration() } - + } diff --git a/WeTransferTests/Board/AddFilesTests.swift b/WeTransferTests/Board/AddFilesTests.swift index cd49cd3..9c04ef3 100644 --- a/WeTransferTests/Board/AddFilesTests.swift +++ b/WeTransferTests/Board/AddFilesTests.swift @@ -10,113 +10,113 @@ import XCTest @testable import WeTransfer final class AddFilesTests: BaseTestCase { - - func testAddingFilesToBoardModel() { - - let transfer = Board(name: "Test Transfer", description: nil) - guard let file = TestConfiguration.fileModel else { - XCTFail("Could not create file model") - return - } - - transfer.add([file]) - XCTAssertTrue(transfer.files.contains(file)) - XCTAssertEqual(transfer.files.count, 1) - - transfer.add([file]) - XCTAssertEqual(transfer.files.count, 1) - } - - func testFileModel() { - guard TestConfiguration.imageFileURL != nil else { - XCTFail("Test image not found") - return - } - - guard let file = TestConfiguration.fileModel else { - XCTFail("Could not create file model") - return - } - - XCTAssertNil(file.identifier) - XCTAssertFalse(file.isUploaded) - XCTAssertNil(file.numberOfChunks) - XCTAssertNil(file.multipartUploadIdentifier) - XCTAssertEqual(file.filesize, 1200480, "File size not equal") - } - - func testAddFilesRequest() { - let board = Board(name: "Test Transfer", description: nil) - - guard let file = TestConfiguration.fileModel else { - XCTFail("File not available") - return - } - - let addedFilesExpectation = expectation(description: "Files are added") - - WeTransfer.add([file], to: board, completion: { (result) in - if case .failure(let error) = result { - XCTFail("Add files to transfer failed: \(error)") - } - addedFilesExpectation.fulfill() - }) - - waitForExpectations(timeout: 10) { _ in - XCTAssertFalse(board.files.isEmpty) - for file in board.files { - XCTAssertNotNil(file.identifier) - XCTAssertFalse(file.isUploaded) - } - } - } - - func testMulitpleFileRequests() { - let board = Board(name: "Test Transfer", description: nil) - - guard let file = TestConfiguration.fileModel, let smallFile = TestConfiguration.smallFileModel else { - XCTFail("File not available") - return - } - - let addedFirstFileExpectation = expectation(description: "First file was added") - let addedSecondFileExpectation = expectation(description: "Second file was added") - - WeTransfer.createExternalBoard(board) { result in - if case .failure(let error) = result { - XCTFail("Create transfer failed: \(error)") - return - } - - var firstFileCompleted = false - - WeTransfer.add([file], to: board, completion: { (result) in - if case .failure(let error) = result { - XCTFail("Add files to transfer failed: \(error)") - return - } - firstFileCompleted = true - addedFirstFileExpectation.fulfill() - }) - - // Do the small file second and expect it to be completed after the first file completes - WeTransfer.add([smallFile], to: board, completion: { (result) in - if case .failure(let error) = result { - XCTFail("Add files to transfer failed: \(error)") - return - } - XCTAssertEqual(board.files.count, 2) - XCTAssertTrue(firstFileCompleted) - addedSecondFileExpectation.fulfill() - }) - } - - waitForExpectations(timeout: 10) { _ in - XCTAssertFalse(board.files.isEmpty) - for file in board.files { - XCTAssertNotNil(file.identifier) - XCTAssertFalse(file.isUploaded) - } - } - } + + func testAddingFilesToBoardModel() { + + let transfer = Board(name: "Test Transfer", description: nil) + guard let file = TestConfiguration.fileModel else { + XCTFail("Could not create file model") + return + } + + transfer.add([file]) + XCTAssertTrue(transfer.files.contains(file)) + XCTAssertEqual(transfer.files.count, 1) + + transfer.add([file]) + XCTAssertEqual(transfer.files.count, 1) + } + + func testFileModel() { + guard TestConfiguration.imageFileURL != nil else { + XCTFail("Test image not found") + return + } + + guard let file = TestConfiguration.fileModel else { + XCTFail("Could not create file model") + return + } + + XCTAssertNil(file.identifier) + XCTAssertFalse(file.isUploaded) + XCTAssertNil(file.numberOfChunks) + XCTAssertNil(file.multipartUploadIdentifier) + XCTAssertEqual(file.filesize, 1200480, "File size not equal") + } + + func testAddFilesRequest() { + let board = Board(name: "Test Transfer", description: nil) + + guard let file = TestConfiguration.fileModel else { + XCTFail("File not available") + return + } + + let addedFilesExpectation = expectation(description: "Files are added") + + WeTransfer.add([file], to: board, completion: { (result) in + if case .failure(let error) = result { + XCTFail("Add files to transfer failed: \(error)") + } + addedFilesExpectation.fulfill() + }) + + waitForExpectations(timeout: 10) { _ in + XCTAssertFalse(board.files.isEmpty) + for file in board.files { + XCTAssertNotNil(file.identifier) + XCTAssertFalse(file.isUploaded) + } + } + } + + func testMulitpleFileRequests() { + let board = Board(name: "Test Transfer", description: nil) + + guard let file = TestConfiguration.fileModel, let smallFile = TestConfiguration.smallFileModel else { + XCTFail("File not available") + return + } + + let addedFirstFileExpectation = expectation(description: "First file was added") + let addedSecondFileExpectation = expectation(description: "Second file was added") + + WeTransfer.createExternalBoard(board) { result in + if case .failure(let error) = result { + XCTFail("Create transfer failed: \(error)") + return + } + + var firstFileCompleted = false + + WeTransfer.add([file], to: board, completion: { (result) in + if case .failure(let error) = result { + XCTFail("Add files to transfer failed: \(error)") + return + } + firstFileCompleted = true + addedFirstFileExpectation.fulfill() + }) + + // Do the small file second and expect it to be completed after the first file completes + WeTransfer.add([smallFile], to: board, completion: { (result) in + if case .failure(let error) = result { + XCTFail("Add files to transfer failed: \(error)") + return + } + XCTAssertEqual(board.files.count, 2) + XCTAssertTrue(firstFileCompleted) + addedSecondFileExpectation.fulfill() + }) + } + + waitForExpectations(timeout: 10) { _ in + XCTAssertFalse(board.files.isEmpty) + for file in board.files { + XCTAssertNotNil(file.identifier) + XCTAssertFalse(file.isUploaded) + } + } + } } diff --git a/WeTransferTests/Board/BoardChunksTests.swift b/WeTransferTests/Board/BoardChunksTests.swift index 5dec94c..704c703 100644 --- a/WeTransferTests/Board/BoardChunksTests.swift +++ b/WeTransferTests/Board/BoardChunksTests.swift @@ -10,55 +10,55 @@ import XCTest @testable import WeTransfer final class BoardChunksTests: BaseTestCase { - - func testChunkCreationRequest() { - guard let file = TestConfiguration.fileModel else { - XCTFail("File not available") - return - } - - var updatedFile: File? - var createdChunk: Chunk? - - let createdChunksExpectation = expectation(description: "Chunks are created") - - let board = Board(name: "Test board", description: nil) - WeTransfer.add([file], to: board) { (result) in - switch result { - case .failure(let error): - XCTFail("Creating board failed: \(error)") - createdChunksExpectation.fulfill() - return - case .success(let board): - updatedFile = board.files.first - guard let file = updatedFile else { - XCTFail("File not added to transfer") - createdChunksExpectation.fulfill() - return - } - let operation = CreateChunkOperation(container: board, file: file, chunkIndex: 0) - operation.onResult = { result in - switch result { - case .failure(let error): - XCTFail("Creating chunk failed: \(error)") - case .success(let chunk): - createdChunk = chunk - } - createdChunksExpectation.fulfill() - } - WeTransfer.client.operationQueue.addOperation(operation) - } - } - - waitForExpectations(timeout: 10) { _ in - guard let file = updatedFile else { - XCTFail("File not created") - return - } - XCTAssertNotNil(file.numberOfChunks, "File object doesn't have numberOfChunks") - XCTAssertNotNil(file.multipartUploadIdentifier, "File object doesn't have a multipart upload identifier") - XCTAssertEqual(file.numberOfChunks, Int(ceil(Double(file.filesize) / Double(file.chunkSize ?? Chunk.defaultChunkSize))), "File doesn't have correct number of chunks") - XCTAssertNotNil(createdChunk, "Chunk not created") - } - } + + func testChunkCreationRequest() { + guard let file = TestConfiguration.fileModel else { + XCTFail("File not available") + return + } + + var updatedFile: File? + var createdChunk: Chunk? + + let createdChunksExpectation = expectation(description: "Chunks are created") + + let board = Board(name: "Test board", description: nil) + WeTransfer.add([file], to: board) { (result) in + switch result { + case .failure(let error): + XCTFail("Creating board failed: \(error)") + createdChunksExpectation.fulfill() + return + case .success(let board): + updatedFile = board.files.first + guard let file = updatedFile else { + XCTFail("File not added to transfer") + createdChunksExpectation.fulfill() + return + } + let operation = CreateChunkOperation(container: board, file: file, chunkIndex: 0) + operation.onResult = { result in + switch result { + case .failure(let error): + XCTFail("Creating chunk failed: \(error)") + case .success(let chunk): + createdChunk = chunk + } + createdChunksExpectation.fulfill() + } + WeTransfer.client.operationQueue.addOperation(operation) + } + } + + waitForExpectations(timeout: 10) { _ in + guard let file = updatedFile else { + XCTFail("File not created") + return + } + XCTAssertNotNil(file.numberOfChunks, "File object doesn't have numberOfChunks") + XCTAssertNotNil(file.multipartUploadIdentifier, "File object doesn't have a multipart upload identifier") + XCTAssertEqual(file.numberOfChunks, Int(ceil(Double(file.filesize) / Double(file.chunkSize ?? Chunk.defaultChunkSize))), "File doesn't have correct number of chunks") + XCTAssertNotNil(createdChunk, "Chunk not created") + } + } } diff --git a/WeTransferTests/Board/BoardUploadTests.swift b/WeTransferTests/Board/BoardUploadTests.swift index 027848b..cebb36e 100644 --- a/WeTransferTests/Board/BoardUploadTests.swift +++ b/WeTransferTests/Board/BoardUploadTests.swift @@ -10,57 +10,57 @@ import XCTest @testable import WeTransfer final class BoardUploadTests: BaseTestCase { - - var observation: NSKeyValueObservation? - - func testFileUpload() { - - guard let file = TestConfiguration.fileModel else { - XCTFail("File not available") - return - } - - let filesUploadedExpectation = expectation(description: "Files are uploaded") - - let board = Board(name: "Test Board", description: nil) - WeTransfer.add([file], to: board) { (result) in - if case .failure(let error) = result { - XCTFail("Adding files failed: \(error)") - filesUploadedExpectation.fulfill() - } - - WeTransfer.upload(board, stateChanged: { (state) in - switch state { - case .created(let board): - print("Transfer created: \(String(describing: board.identifier))") - case .uploading(let progress): - print("Upload started") - var percentage = 0.0 - self.observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, _) in - let newPercentage = (progress.fractionCompleted * 100).rounded(FloatingPointRoundingRule.up) - if newPercentage != percentage { - percentage = newPercentage - print("PROGRESS: \(newPercentage)% (\(progress.completedUnitCount) bytes)") - } - }) - case .failed(let error): - XCTFail("Sending transfer failed: \(error)") - filesUploadedExpectation.fulfill() - case .completed: - filesUploadedExpectation.fulfill() - } - }) - } - - waitForExpectations(timeout: 60) { _ in - self.observation = nil - if let url = board.shortURL { - print("Transfer uploaded: \(url)") - } - XCTAssertNotNil(board.shortURL) - for file in board.files { - XCTAssertTrue(file.isUploaded) - } - } - } + + var observation: NSKeyValueObservation? + + func testFileUpload() { + + guard let file = TestConfiguration.fileModel else { + XCTFail("File not available") + return + } + + let filesUploadedExpectation = expectation(description: "Files are uploaded") + + let board = Board(name: "Test Board", description: nil) + WeTransfer.add([file], to: board) { (result) in + if case .failure(let error) = result { + XCTFail("Adding files failed: \(error)") + filesUploadedExpectation.fulfill() + } + + WeTransfer.upload(board, stateChanged: { (state) in + switch state { + case .created(let board): + print("Transfer created: \(String(describing: board.identifier))") + case .uploading(let progress): + print("Upload started") + var percentage = 0.0 + self.observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, _) in + let newPercentage = (progress.fractionCompleted * 100).rounded(FloatingPointRoundingRule.up) + if newPercentage != percentage { + percentage = newPercentage + print("PROGRESS: \(newPercentage)% (\(progress.completedUnitCount) bytes)") + } + }) + case .failed(let error): + XCTFail("Sending transfer failed: \(error)") + filesUploadedExpectation.fulfill() + case .completed: + filesUploadedExpectation.fulfill() + } + }) + } + + waitForExpectations(timeout: 60) { _ in + self.observation = nil + if let url = board.shortURL { + print("Transfer uploaded: \(url)") + } + XCTAssertNotNil(board.shortURL) + for file in board.files { + XCTAssertTrue(file.isUploaded) + } + } + } } diff --git a/WeTransferTests/Board/CreateBoardTests.swift b/WeTransferTests/Board/CreateBoardTests.swift index 8b39b1d..bce3979 100644 --- a/WeTransferTests/Board/CreateBoardTests.swift +++ b/WeTransferTests/Board/CreateBoardTests.swift @@ -10,18 +10,18 @@ import XCTest @testable import WeTransfer final class CreateBoardTests: BaseTestCase { - - func testCreateBoardRequest() { - let createdBoardExpectation = expectation(description: "Transfer is created") - let board = Board(name: "Test Transfer", description: nil) - WeTransfer.createExternalBoard(board, completion: { result in - if case .failure(let error) = result { - XCTFail("\(error.localizedDescription)") - } - createdBoardExpectation.fulfill() - }) - waitForExpectations(timeout: 10) { _ in - XCTAssertNotNil(board.identifier) - } - } + + func testCreateBoardRequest() { + let createdBoardExpectation = expectation(description: "Transfer is created") + let board = Board(name: "Test Transfer", description: nil) + WeTransfer.createExternalBoard(board, completion: { result in + if case .failure(let error) = result { + XCTFail("\(error.localizedDescription)") + } + createdBoardExpectation.fulfill() + }) + waitForExpectations(timeout: 10) { _ in + XCTAssertNotNil(board.identifier) + } + } } diff --git a/WeTransferTests/Board/SimpleBoardUploadTests.swift b/WeTransferTests/Board/SimpleBoardUploadTests.swift index d615e80..3b01d16 100644 --- a/WeTransferTests/Board/SimpleBoardUploadTests.swift +++ b/WeTransferTests/Board/SimpleBoardUploadTests.swift @@ -10,44 +10,44 @@ import XCTest @testable import WeTransfer final class SimpleBoardUploadTests: BaseTestCase { - - func testSimpleUpload() { - - guard let fileURL = TestConfiguration.imageFileURL else { - XCTFail("Test image not found") - return - } - - let simpleBoardUploadExpectation = expectation(description: "All files are uploaded") - var updatedBoard: Board? - var timer: Timer? - - WeTransfer.uploadBoard(named: "Test board", description: nil, containing: [fileURL]) { state in - switch state { - case .created(let board): - print("Board created: \(board)") - case .uploading(let progress): - print("Uploading files...") - timer = Timer(timeInterval: 1 / 30, repeats: true, block: { _ in - print("Progress: \(progress.fractionCompleted)") - }) - RunLoop.main.add(timer!, forMode: RunLoop.Mode.common) - case .completed(let board): - timer?.invalidate() - timer = nil - print("Files uploaded: \(String(describing: board.shortURL))") - updatedBoard = board - simpleBoardUploadExpectation.fulfill() - case .failed(let error): - timer?.invalidate() - timer = nil - XCTFail("Creation/upload failed: \(error)") - simpleBoardUploadExpectation.fulfill() - } - } - - waitForExpectations(timeout: 60) { _ in - XCTAssertNotNil(updatedBoard, "Board upload was not completed") - } - } + + func testSimpleUpload() { + + guard let fileURL = TestConfiguration.imageFileURL else { + XCTFail("Test image not found") + return + } + + let simpleBoardUploadExpectation = expectation(description: "All files are uploaded") + var updatedBoard: Board? + var timer: Timer? + + WeTransfer.uploadBoard(named: "Test board", description: nil, containing: [fileURL]) { state in + switch state { + case .created(let board): + print("Board created: \(board)") + case .uploading(let progress): + print("Uploading files...") + timer = Timer(timeInterval: 1 / 30, repeats: true, block: { _ in + print("Progress: \(progress.fractionCompleted)") + }) + RunLoop.main.add(timer!, forMode: RunLoop.Mode.common) + case .completed(let board): + timer?.invalidate() + timer = nil + print("Files uploaded: \(String(describing: board.shortURL))") + updatedBoard = board + simpleBoardUploadExpectation.fulfill() + case .failed(let error): + timer?.invalidate() + timer = nil + XCTFail("Creation/upload failed: \(error)") + simpleBoardUploadExpectation.fulfill() + } + } + + waitForExpectations(timeout: 60) { _ in + XCTAssertNotNil(updatedBoard, "Board upload was not completed") + } + } } diff --git a/WeTransferTests/InitializationTests.swift b/WeTransferTests/InitializationTests.swift index f95a224..62c72bb 100644 --- a/WeTransferTests/InitializationTests.swift +++ b/WeTransferTests/InitializationTests.swift @@ -10,23 +10,23 @@ import XCTest @testable import WeTransfer final class InitializationTests: XCTestCase { - - override func tearDown() { - super.tearDown() - TestConfiguration.resetConfiguration() - } - - func testNotConfigured() { - do { - _ = try WeTransfer.client.createRequest(.createTransfer()) - XCTFail("Creation of request should've failed") - } catch { - XCTAssertEqual(error.localizedDescription, WeTransfer.Error.notConfigured.localizedDescription) - } - } - - func testConfigure() { - TestConfiguration.configure(environment: .production) - XCTAssertNotNil(WeTransfer.client.apiKey, "APIKey needs to be set") - } + + override func tearDown() { + super.tearDown() + TestConfiguration.resetConfiguration() + } + + func testNotConfigured() { + do { + _ = try WeTransfer.client.createRequest(.createTransfer()) + XCTFail("Creation of request should've failed") + } catch { + XCTAssertEqual(error.localizedDescription, WeTransfer.Error.notConfigured.localizedDescription) + } + } + + func testConfigure() { + TestConfiguration.configure(environment: .production) + XCTAssertNotNil(WeTransfer.client.apiKey, "APIKey needs to be set") + } } diff --git a/WeTransferTests/RequestTests.swift b/WeTransferTests/RequestTests.swift index aac15f2..379c7e9 100644 --- a/WeTransferTests/RequestTests.swift +++ b/WeTransferTests/RequestTests.swift @@ -10,78 +10,78 @@ import XCTest @testable import WeTransfer final class RequestTests: XCTestCase { - - override func tearDown() { - super.tearDown() - TestConfiguration.resetConfiguration() - } - - func testEndpoints() { - let baseURLString = "https://dev.wetransfer.com/v2/" - let baseURL = URL(string: baseURLString)! - - let authorizeEndpoint: APIEndpoint = .authorize() - XCTAssertEqual(authorizeEndpoint.method, .post) - XCTAssertEqual(authorizeEndpoint.url(with: baseURL).absoluteString, baseURLString + "authorize") - - let createTransferEndpoint: APIEndpoint = .createTransfer() - XCTAssertEqual(createTransferEndpoint.method, .post) - XCTAssertEqual(createTransferEndpoint.url(with: baseURL).absoluteString, baseURLString + "transfers") - - let createBoardEndpoint: APIEndpoint = .createBoard() - XCTAssertEqual(createBoardEndpoint.method, .post) - XCTAssertEqual(createBoardEndpoint.url(with: baseURL).absoluteString, baseURLString + "boards") - - let transferIdentifier = UUID().uuidString - let boardIdentifier = UUID().uuidString - let fileIdentifier = UUID().uuidString - let chunkIndex = 5 - let multipartIdentifier = UUID().uuidString - - let addFilesTransferEndpoint: APIEndpoint = .addFiles(boardIdentifier: boardIdentifier) - XCTAssertEqual(addFilesTransferEndpoint.method, .post) - XCTAssertEqual(addFilesTransferEndpoint.url(with: baseURL).absoluteString, baseURLString + "boards/\(boardIdentifier)/files") - - let requestTransferUploadURLEndpoint: APIEndpoint = .requestTransferUploadURL(transferIdentifier: transferIdentifier, fileIdentifier: fileIdentifier, chunkIndex: chunkIndex) - XCTAssertEqual(requestTransferUploadURLEndpoint.method, .get) - XCTAssertEqual(requestTransferUploadURLEndpoint.url(with: baseURL).absoluteString, baseURLString + "transfers/\(transferIdentifier)/files/\(fileIdentifier)/upload-url/\(chunkIndex + 1)") - - let requestBoardUploadURLEndpoint: APIEndpoint = .requestBoardUploadURL(boardIdentifier: boardIdentifier, fileIdentifier: fileIdentifier, chunkIndex: chunkIndex, multipartIdentifier: multipartIdentifier) - XCTAssertEqual(requestBoardUploadURLEndpoint.method, .get) - XCTAssertEqual(requestBoardUploadURLEndpoint.url(with: baseURL).absoluteString, baseURLString + "boards/\(boardIdentifier)/files/\(fileIdentifier)/upload-url/\(chunkIndex + 1)/\(multipartIdentifier)") - - let completeTransferFileUploadEndpoint: APIEndpoint = .completeTransferFileUpload(transferIdentifier: transferIdentifier, fileIdentifier: fileIdentifier) - XCTAssertEqual(completeTransferFileUploadEndpoint.method, .put) - XCTAssertEqual(completeTransferFileUploadEndpoint.url(with: baseURL).absoluteString, baseURLString + "transfers/\(transferIdentifier)/files/\(fileIdentifier)/upload-complete") - - let completeBoardFileUploadEndpoint: APIEndpoint = .completeBoardFileUpload(boardIdentifier: boardIdentifier, fileIdentifier: fileIdentifier) - XCTAssertEqual(completeBoardFileUploadEndpoint.method, .put) - XCTAssertEqual(completeBoardFileUploadEndpoint.url(with: baseURL).absoluteString, baseURLString + "boards/\(boardIdentifier)/files/\(fileIdentifier)/upload-complete") - - let finlizeTransferEndpoint: APIEndpoint = .finalizeTransfer(transferIdentifier: transferIdentifier) - XCTAssertEqual(finlizeTransferEndpoint.method, .put) - XCTAssertEqual(finlizeTransferEndpoint.url(with: baseURL).absoluteString, baseURLString + "transfers/\(transferIdentifier)/finalize") - } - - func testUnconfiguredRequestCreation() { - do { - _ = try WeTransfer.client.createRequest(.createTransfer()) - XCTFail("Request creation should have failed with 'not configured' error") - } catch { - XCTAssertEqual(error.localizedDescription, WeTransfer.Error.notConfigured.localizedDescription) - } - } - - func testRequestCreation() { - TestConfiguration.configure(environment: .production) - TestConfiguration.fakeAuthorize() - let client = WeTransfer.client - - do { - _ = try client.createRequest(.createTransfer()) - } catch { - XCTFail(error.localizedDescription) - } - } - + + override func tearDown() { + super.tearDown() + TestConfiguration.resetConfiguration() + } + + func testEndpoints() { + let baseURLString = "https://dev.wetransfer.com/v2/" + let baseURL = URL(string: baseURLString)! + + let authorizeEndpoint: APIEndpoint = .authorize() + XCTAssertEqual(authorizeEndpoint.method, .post) + XCTAssertEqual(authorizeEndpoint.url(with: baseURL).absoluteString, baseURLString + "authorize") + + let createTransferEndpoint: APIEndpoint = .createTransfer() + XCTAssertEqual(createTransferEndpoint.method, .post) + XCTAssertEqual(createTransferEndpoint.url(with: baseURL).absoluteString, baseURLString + "transfers") + + let createBoardEndpoint: APIEndpoint = .createBoard() + XCTAssertEqual(createBoardEndpoint.method, .post) + XCTAssertEqual(createBoardEndpoint.url(with: baseURL).absoluteString, baseURLString + "boards") + + let transferIdentifier = UUID().uuidString + let boardIdentifier = UUID().uuidString + let fileIdentifier = UUID().uuidString + let chunkIndex = 5 + let multipartIdentifier = UUID().uuidString + + let addFilesTransferEndpoint: APIEndpoint = .addFiles(boardIdentifier: boardIdentifier) + XCTAssertEqual(addFilesTransferEndpoint.method, .post) + XCTAssertEqual(addFilesTransferEndpoint.url(with: baseURL).absoluteString, baseURLString + "boards/\(boardIdentifier)/files") + + let requestTransferUploadURLEndpoint: APIEndpoint = .requestTransferUploadURL(transferIdentifier: transferIdentifier, fileIdentifier: fileIdentifier, chunkIndex: chunkIndex) + XCTAssertEqual(requestTransferUploadURLEndpoint.method, .get) + XCTAssertEqual(requestTransferUploadURLEndpoint.url(with: baseURL).absoluteString, baseURLString + "transfers/\(transferIdentifier)/files/\(fileIdentifier)/upload-url/\(chunkIndex + 1)") + + let requestBoardUploadURLEndpoint: APIEndpoint = .requestBoardUploadURL(boardIdentifier: boardIdentifier, fileIdentifier: fileIdentifier, chunkIndex: chunkIndex, multipartIdentifier: multipartIdentifier) + XCTAssertEqual(requestBoardUploadURLEndpoint.method, .get) + XCTAssertEqual(requestBoardUploadURLEndpoint.url(with: baseURL).absoluteString, baseURLString + "boards/\(boardIdentifier)/files/\(fileIdentifier)/upload-url/\(chunkIndex + 1)/\(multipartIdentifier)") + + let completeTransferFileUploadEndpoint: APIEndpoint = .completeTransferFileUpload(transferIdentifier: transferIdentifier, fileIdentifier: fileIdentifier) + XCTAssertEqual(completeTransferFileUploadEndpoint.method, .put) + XCTAssertEqual(completeTransferFileUploadEndpoint.url(with: baseURL).absoluteString, baseURLString + "transfers/\(transferIdentifier)/files/\(fileIdentifier)/upload-complete") + + let completeBoardFileUploadEndpoint: APIEndpoint = .completeBoardFileUpload(boardIdentifier: boardIdentifier, fileIdentifier: fileIdentifier) + XCTAssertEqual(completeBoardFileUploadEndpoint.method, .put) + XCTAssertEqual(completeBoardFileUploadEndpoint.url(with: baseURL).absoluteString, baseURLString + "boards/\(boardIdentifier)/files/\(fileIdentifier)/upload-complete") + + let finlizeTransferEndpoint: APIEndpoint = .finalizeTransfer(transferIdentifier: transferIdentifier) + XCTAssertEqual(finlizeTransferEndpoint.method, .put) + XCTAssertEqual(finlizeTransferEndpoint.url(with: baseURL).absoluteString, baseURLString + "transfers/\(transferIdentifier)/finalize") + } + + func testUnconfiguredRequestCreation() { + do { + _ = try WeTransfer.client.createRequest(.createTransfer()) + XCTFail("Request creation should have failed with 'not configured' error") + } catch { + XCTAssertEqual(error.localizedDescription, WeTransfer.Error.notConfigured.localizedDescription) + } + } + + func testRequestCreation() { + TestConfiguration.configure(environment: .production) + TestConfiguration.fakeAuthorize() + let client = WeTransfer.client + + do { + _ = try client.createRequest(.createTransfer()) + } catch { + XCTFail(error.localizedDescription) + } + } + } diff --git a/WeTransferTests/Secrets.swift b/WeTransferTests/Secrets.swift index 7839137..317886a 100644 --- a/WeTransferTests/Secrets.swift +++ b/WeTransferTests/Secrets.swift @@ -9,46 +9,46 @@ import Foundation struct Secrets { - - private enum SecretName: String { - case productionKey = "production" - case stagingKey = "staging" - case stagingURL = "staging-url" - } - - /* API keys are retrieved from 'Secrets.plist' which is used for running tests on travis. - For local testing, please either manually add Secrets.plist with at least a 'production' key or replace the value below with your API key */ - static var productionKey: String { - guard let value = value(for: .productionKey) else { - fatalError("Production API key not found in Secrets.plist. Provide your own Secrets.plist with '\(SecretName.productionKey.rawValue)' or manually replace variable here") - } - return value - } - - static var stagingKey: String { - guard let value = value(for: .stagingKey) else { - fatalError("Staging API key not found in Secrets.plist. Provide your own Secrets.plist with '\(SecretName.stagingKey.rawValue)' or manually replace variable here") - } - return value - } - - static var stagingURL: URL { - guard let value = value(for: .stagingURL), let url = URL(string: value) else { - fatalError("Staging URL not found in Secrets.plist. Provide your own Secrets.plist with '\(SecretName.stagingURL.rawValue)' or manually replace variable here") - } - return url - } + + private enum SecretName: String { + case productionKey = "production" + case stagingKey = "staging" + case stagingURL = "staging-url" + } + + /* API keys are retrieved from 'Secrets.plist' which is used for running tests on travis. + For local testing, please either manually add Secrets.plist with at least a 'production' key or replace the value below with your API key */ + static var productionKey: String { + guard let value = value(for: .productionKey) else { + fatalError("Production API key not found in Secrets.plist. Provide your own Secrets.plist with '\(SecretName.productionKey.rawValue)' or manually replace variable here") + } + return value + } + + static var stagingKey: String { + guard let value = value(for: .stagingKey) else { + fatalError("Staging API key not found in Secrets.plist. Provide your own Secrets.plist with '\(SecretName.stagingKey.rawValue)' or manually replace variable here") + } + return value + } + + static var stagingURL: URL { + guard let value = value(for: .stagingURL), let url = URL(string: value) else { + fatalError("Staging URL not found in Secrets.plist. Provide your own Secrets.plist with '\(SecretName.stagingURL.rawValue)' or manually replace variable here") + } + return url + } } extension Secrets { - private static var plistURL: URL? { - return Bundle(for: TestConfiguration.shared.classForCoder).url(forResource: "Secrets", withExtension: "plist") - } - - private static func value(for secret: SecretName) -> String? { - guard let url = plistURL, let keysDictionary = NSDictionary(contentsOf: url) else { - return nil - } - return keysDictionary[secret.rawValue] as? String - } + private static var plistURL: URL? { + return Bundle(for: TestConfiguration.shared.classForCoder).url(forResource: "Secrets", withExtension: "plist") + } + + private static func value(for secret: SecretName) -> String? { + guard let url = plistURL, let keysDictionary = NSDictionary(contentsOf: url) else { + return nil + } + return keysDictionary[secret.rawValue] as? String + } } diff --git a/WeTransferTests/TestConfiguration.swift b/WeTransferTests/TestConfiguration.swift index dae59c2..e7eebb9 100644 --- a/WeTransferTests/TestConfiguration.swift +++ b/WeTransferTests/TestConfiguration.swift @@ -10,56 +10,56 @@ import Foundation @testable import WeTransfer final class TestConfiguration: NSObject { - - enum Environment { - case production - case staging - } - - static func configure(environment: Environment) { - let configuration: WeTransfer.Configuration - - switch environment { - case .production: - configuration = WeTransfer.Configuration(apiKey: Secrets.productionKey) - case .staging: - configuration = WeTransfer.Configuration(apiKey: Secrets.stagingKey, baseURL: Secrets.stagingURL) - } - WeTransfer.configure(with: configuration) - } - - static func fakeAuthorize() { - WeTransfer.client.authenticator.updateBearer("Fake.Tokens.Gonna-Fake") - } - - static func resetConfiguration() { - WeTransfer.client.apiKey = nil - WeTransfer.client.authenticator.updateBearer(nil) - } + + enum Environment { + case production + case staging + } + + static func configure(environment: Environment) { + let configuration: WeTransfer.Configuration + + switch environment { + case .production: + configuration = WeTransfer.Configuration(apiKey: Secrets.productionKey) + case .staging: + configuration = WeTransfer.Configuration(apiKey: Secrets.stagingKey, baseURL: Secrets.stagingURL) + } + WeTransfer.configure(with: configuration) + } + + static func fakeAuthorize() { + WeTransfer.client.authenticator.updateBearer("Fake.Tokens.Gonna-Fake") + } + + static func resetConfiguration() { + WeTransfer.client.apiKey = nil + WeTransfer.client.authenticator.updateBearer(nil) + } } extension TestConfiguration { - static let shared = TestConfiguration() - - final class var imageFileURL: URL? { - return Bundle(for: self.shared.classForCoder).url(forResource: "image", withExtension: "jpg") - } - - final class var fileModel: File? { - guard let imageFileURL = imageFileURL else { - return nil - } - return try? File(url: imageFileURL) - } - - final class var smallImageFileURL: URL? { - return Bundle(for: self.shared.classForCoder).url(forResource: "smallImage", withExtension: "jpg") - } - - final class var smallFileModel: File? { - guard let imageFileURL = smallImageFileURL else { - return nil - } - return try? File(url: imageFileURL) - } + static let shared = TestConfiguration() + + final class var imageFileURL: URL? { + return Bundle(for: self.shared.classForCoder).url(forResource: "image", withExtension: "jpg") + } + + final class var fileModel: File? { + guard let imageFileURL = imageFileURL else { + return nil + } + return try? File(url: imageFileURL) + } + + final class var smallImageFileURL: URL? { + return Bundle(for: self.shared.classForCoder).url(forResource: "smallImage", withExtension: "jpg") + } + + final class var smallFileModel: File? { + guard let imageFileURL = smallImageFileURL else { + return nil + } + return try? File(url: imageFileURL) + } } diff --git a/WeTransferTests/Transfer/CreateTransferTests.swift b/WeTransferTests/Transfer/CreateTransferTests.swift index 90861b7..8bfe2fd 100644 --- a/WeTransferTests/Transfer/CreateTransferTests.swift +++ b/WeTransferTests/Transfer/CreateTransferTests.swift @@ -10,35 +10,35 @@ import XCTest @testable import WeTransfer final class CreateTransferTests: BaseTestCase { - - func testCreateTransferRequest() { - guard let fileURL = TestConfiguration.imageFileURL else { - XCTFail("File not available") - return - } - - let createdTransferExpectation = expectation(description: "Transfer is created") - var transferResult: Transfer? - WeTransfer.createTransfer(saying: "Test transfer", fileURLs: [fileURL]) { result in - switch result { - case .success(let transfer): - transferResult = transfer - case .failure(let error): - XCTFail("\(error.localizedDescription)") - } - createdTransferExpectation.fulfill() - } - - waitForExpectations(timeout: 10) { _ in - XCTAssertNotNil(transferResult) - if let transfer = transferResult { - XCTAssertFalse(transfer.files.isEmpty) - for file in transfer.files { - XCTAssertNotNil(file.identifier) - XCTAssertFalse(file.isUploaded) - XCTAssertNotNil(file.numberOfChunks) - } - } - } - } + + func testCreateTransferRequest() { + guard let fileURL = TestConfiguration.imageFileURL else { + XCTFail("File not available") + return + } + + let createdTransferExpectation = expectation(description: "Transfer is created") + var transferResult: Transfer? + WeTransfer.createTransfer(saying: "Test transfer", fileURLs: [fileURL]) { result in + switch result { + case .success(let transfer): + transferResult = transfer + case .failure(let error): + XCTFail("\(error.localizedDescription)") + } + createdTransferExpectation.fulfill() + } + + waitForExpectations(timeout: 10) { _ in + XCTAssertNotNil(transferResult) + if let transfer = transferResult { + XCTAssertFalse(transfer.files.isEmpty) + for file in transfer.files { + XCTAssertNotNil(file.identifier) + XCTAssertFalse(file.isUploaded) + XCTAssertNotNil(file.numberOfChunks) + } + } + } + } } diff --git a/WeTransferTests/Transfer/SimpleTransferTests.swift b/WeTransferTests/Transfer/SimpleTransferTests.swift index 2088663..86659d7 100644 --- a/WeTransferTests/Transfer/SimpleTransferTests.swift +++ b/WeTransferTests/Transfer/SimpleTransferTests.swift @@ -10,46 +10,46 @@ import XCTest @testable import WeTransfer final class SimpleTransferTests: BaseTestCase { - - func testSimpleTransfer() { - - guard let fileURL = TestConfiguration.imageFileURL else { - XCTFail("Test image not found") - return - } - - let simpleTransferExpectation = expectation(description: "Transfer has been sent") - var updatedTransfer: Transfer? - var timer: Timer? - - WeTransfer.uploadTransfer(saying: "Test transfer", containing: [fileURL]) { state in - switch state { - case .created(let transfer): - print("Transfer created: \(transfer)") - case .uploading(let progress): - print("Transfer started...") - timer = Timer(timeInterval: 1 / 30, repeats: true, block: { _ in - print("Progress: \(progress.fractionCompleted)") - }) - RunLoop.main.add(timer!, forMode: RunLoop.Mode.common) - case .completed(let transfer): - timer?.invalidate() - timer = nil - print("Transfer sent: \(String(describing: transfer.shortURL))") - updatedTransfer = transfer - simpleTransferExpectation.fulfill() - case .failed(let error): - timer?.invalidate() - timer = nil - XCTFail("Transfer failed: \(error)") - simpleTransferExpectation.fulfill() - } - } - - waitForExpectations(timeout: 60) { _ in - XCTAssertNotNil(updatedTransfer, "Transfer was not completed") - XCTAssertNotNil(updatedTransfer?.shortURL, "Transfer should have a URL when uploaded") - } - } - + + func testSimpleTransfer() { + + guard let fileURL = TestConfiguration.imageFileURL else { + XCTFail("Test image not found") + return + } + + let simpleTransferExpectation = expectation(description: "Transfer has been sent") + var updatedTransfer: Transfer? + var timer: Timer? + + WeTransfer.uploadTransfer(saying: "Test transfer", containing: [fileURL]) { state in + switch state { + case .created(let transfer): + print("Transfer created: \(transfer)") + case .uploading(let progress): + print("Transfer started...") + timer = Timer(timeInterval: 1 / 30, repeats: true, block: { _ in + print("Progress: \(progress.fractionCompleted)") + }) + RunLoop.main.add(timer!, forMode: RunLoop.Mode.common) + case .completed(let transfer): + timer?.invalidate() + timer = nil + print("Transfer sent: \(String(describing: transfer.shortURL))") + updatedTransfer = transfer + simpleTransferExpectation.fulfill() + case .failed(let error): + timer?.invalidate() + timer = nil + XCTFail("Transfer failed: \(error)") + simpleTransferExpectation.fulfill() + } + } + + waitForExpectations(timeout: 60) { _ in + XCTAssertNotNil(updatedTransfer, "Transfer was not completed") + XCTAssertNotNil(updatedTransfer?.shortURL, "Transfer should have a URL when uploaded") + } + } + } diff --git a/WeTransferTests/Transfer/TransferChunksTests.swift b/WeTransferTests/Transfer/TransferChunksTests.swift index 3058a99..9d97b4c 100644 --- a/WeTransferTests/Transfer/TransferChunksTests.swift +++ b/WeTransferTests/Transfer/TransferChunksTests.swift @@ -10,56 +10,56 @@ import XCTest @testable import WeTransfer final class TransferChunksTests: BaseTestCase { - - func testChunkCreationRequest() { - guard let fileURL = TestConfiguration.imageFileURL else { - XCTFail("File not available") - return - } - - var updatedFile: File? - var createdChunk: Chunk? - - let createdChunksExpectation = expectation(description: "Chunks are created") - - WeTransfer.createTransfer(saying: "Test Transfer", fileURLs: [fileURL]) { result in - switch result { - case .failure(let error): - XCTFail("Creating transfer failed: \(error)") - createdChunksExpectation.fulfill() - return - case .success(let transfer): - updatedFile = transfer.files.first - guard let file = updatedFile else { - XCTFail("File not added to transfer") - createdChunksExpectation.fulfill() - return - } - let operation = CreateChunkOperation(container: transfer, file: file, chunkIndex: 0) - operation.onResult = { result in - switch result { - case .failure(let error): - XCTFail("Creating chunk failed: \(error)") - case .success(let chunk): - createdChunk = chunk - } - createdChunksExpectation.fulfill() - } - WeTransfer.client.operationQueue.addOperation(operation) - } - } - - waitForExpectations(timeout: 10) { _ in - guard let file = updatedFile else { - XCTFail("File not created") - return - } - XCTAssertNotNil(file.numberOfChunks, "File object doesn't have numberOfChunks") - XCTAssertNotNil(file.chunkSize, "File object must have a chunkSize") - if let chunkSize = file.chunkSize { - XCTAssertEqual(file.numberOfChunks, Int(ceil(Double(file.filesize) / Double(chunkSize))), "File doesn't have correct number of chunks") - } - XCTAssertNotNil(createdChunk, "Chunk not created") - } - } + + func testChunkCreationRequest() { + guard let fileURL = TestConfiguration.imageFileURL else { + XCTFail("File not available") + return + } + + var updatedFile: File? + var createdChunk: Chunk? + + let createdChunksExpectation = expectation(description: "Chunks are created") + + WeTransfer.createTransfer(saying: "Test Transfer", fileURLs: [fileURL]) { result in + switch result { + case .failure(let error): + XCTFail("Creating transfer failed: \(error)") + createdChunksExpectation.fulfill() + return + case .success(let transfer): + updatedFile = transfer.files.first + guard let file = updatedFile else { + XCTFail("File not added to transfer") + createdChunksExpectation.fulfill() + return + } + let operation = CreateChunkOperation(container: transfer, file: file, chunkIndex: 0) + operation.onResult = { result in + switch result { + case .failure(let error): + XCTFail("Creating chunk failed: \(error)") + case .success(let chunk): + createdChunk = chunk + } + createdChunksExpectation.fulfill() + } + WeTransfer.client.operationQueue.addOperation(operation) + } + } + + waitForExpectations(timeout: 10) { _ in + guard let file = updatedFile else { + XCTFail("File not created") + return + } + XCTAssertNotNil(file.numberOfChunks, "File object doesn't have numberOfChunks") + XCTAssertNotNil(file.chunkSize, "File object must have a chunkSize") + if let chunkSize = file.chunkSize { + XCTAssertEqual(file.numberOfChunks, Int(ceil(Double(file.filesize) / Double(chunkSize))), "File doesn't have correct number of chunks") + } + XCTAssertNotNil(createdChunk, "Chunk not created") + } + } } diff --git a/WeTransferTests/Transfer/TransferUploadTests.swift b/WeTransferTests/Transfer/TransferUploadTests.swift index 51aac96..6adb625 100644 --- a/WeTransferTests/Transfer/TransferUploadTests.swift +++ b/WeTransferTests/Transfer/TransferUploadTests.swift @@ -10,76 +10,76 @@ import XCTest @testable import WeTransfer final class TransferUploadTests: BaseTestCase { - - var observation: NSKeyValueObservation? - - private func createTransfer(completion: @escaping (Transfer?) -> Void) { - guard let file = TestConfiguration.fileModel else { - XCTFail("File not available") - return - } - - WeTransfer.createTransfer(saying: "TestTransfer", fileURLs: [file.url]) { result in - switch result { - case .failure(let error): - XCTFail("Transfer creation failed: \(error)") - completion(nil) - return - case .success(let transfer): - completion(transfer) - } - } - - } - - func testFileUpload() { - - let transferSentExpectation = expectation(description: "Transfer is sent") - var resultTransfer: Transfer? - - createTransfer { (transfer) in - guard let transfer = transfer else { - transferSentExpectation.fulfill() - return - } - resultTransfer = transfer - WeTransfer.upload(transfer, stateChanged: { (state) in - switch state { - case .created(let transfer): - print("Transfer created: \(String(describing: transfer.identifier))") - case .uploading(let progress): - print("Upload started") - var percentage = 0.0 - self.observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, _) in - let newPercentage = (progress.fractionCompleted * 100).rounded(FloatingPointRoundingRule.up) - if newPercentage != percentage { - percentage = newPercentage - print("PROGRESS: \(newPercentage)% (\(progress.completedUnitCount) bytes)") - } - }) - case .failed(let error): - XCTFail("Sending transfer failed: \(error)") - transferSentExpectation.fulfill() - case .completed: - transferSentExpectation.fulfill() - } - }) - } - - waitForExpectations(timeout: 60) { _ in - guard let transfer = resultTransfer else { - XCTFail("No transfer created") - return - } - self.observation = nil - if let url = transfer.shortURL { - print("Transfer uploaded: \(url)") - } - XCTAssertNotNil(transfer.shortURL) - for file in transfer.files { - XCTAssertTrue(file.isUploaded) - } - } - } - + + var observation: NSKeyValueObservation? + + private func createTransfer(completion: @escaping (Transfer?) -> Void) { + guard let file = TestConfiguration.fileModel else { + XCTFail("File not available") + return + } + + WeTransfer.createTransfer(saying: "TestTransfer", fileURLs: [file.url]) { result in + switch result { + case .failure(let error): + XCTFail("Transfer creation failed: \(error)") + completion(nil) + return + case .success(let transfer): + completion(transfer) + } + } + + } + + func testFileUpload() { + + let transferSentExpectation = expectation(description: "Transfer is sent") + var resultTransfer: Transfer? + + createTransfer { (transfer) in + guard let transfer = transfer else { + transferSentExpectation.fulfill() + return + } + resultTransfer = transfer + WeTransfer.upload(transfer, stateChanged: { (state) in + switch state { + case .created(let transfer): + print("Transfer created: \(String(describing: transfer.identifier))") + case .uploading(let progress): + print("Upload started") + var percentage = 0.0 + self.observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, _) in + let newPercentage = (progress.fractionCompleted * 100).rounded(FloatingPointRoundingRule.up) + if newPercentage != percentage { + percentage = newPercentage + print("PROGRESS: \(newPercentage)% (\(progress.completedUnitCount) bytes)") + } + }) + case .failed(let error): + XCTFail("Sending transfer failed: \(error)") + transferSentExpectation.fulfill() + case .completed: + transferSentExpectation.fulfill() + } + }) + } + + waitForExpectations(timeout: 60) { _ in + guard let transfer = resultTransfer else { + XCTFail("No transfer created") + return + } + self.observation = nil + if let url = transfer.shortURL { + print("Transfer uploaded: \(url)") + } + XCTAssertNotNil(transfer.shortURL) + for file in transfer.files { + XCTAssertTrue(file.isUploaded) + } + } + } + } From df11740ea2d15709afe286f6ced90de5ed8f9773 Mon Sep 17 00:00:00 2001 From: Pim Coumans Date: Tue, 6 Nov 2018 13:47:19 +0100 Subject: [PATCH 3/5] Removed merge error --- WeTransfer/Server/Endpoints/Endpoints.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/WeTransfer/Server/Endpoints/Endpoints.swift b/WeTransfer/Server/Endpoints/Endpoints.swift index b0233c6..5bae851 100644 --- a/WeTransfer/Server/Endpoints/Endpoints.swift +++ b/WeTransfer/Server/Endpoints/Endpoints.swift @@ -174,11 +174,6 @@ struct TransferFile: Decodable { /// Parameters used for the create transfer request struct CreateBoardParameters: Encodable { - - struct FileParameters: Encodable { - let name: String - let size: UInt64 - } /// Name of the transfer to create let name: String /// Description of the transfer to create From a85984b0fcd5458593d27d1b59920a07dfac6daa Mon Sep 17 00:00:00 2001 From: Pim Coumans Date: Tue, 6 Nov 2018 14:49:43 +0100 Subject: [PATCH 4/5] Optional file URL --- WeTransfer/Models/Chunk.swift | 19 +++++++++++++++++-- WeTransfer/Models/File.swift | 9 ++++----- .../Operations/CreateChunkOperation.swift | 10 +++++++--- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/WeTransfer/Models/Chunk.swift b/WeTransfer/Models/Chunk.swift index 2f556c5..03bd036 100644 --- a/WeTransfer/Models/Chunk.swift +++ b/WeTransfer/Models/Chunk.swift @@ -11,6 +11,18 @@ import Foundation /// Represents a chunk of data from a file in a transfer or board. Used only in the uploading proces struct Chunk: Encodable { + enum Error: Swift.Error, LocalizedError { + /// Chunk cannot be initialized without a local file + case noFileDataAvailable + + var localizedDescription: String { + switch self { + case .noFileDataAvailable: + return "Provided file is not pointing to a local file" + } + } + } + /// Fallback size chunks except the last, as the last chunk holds the remaining data (filesize % defaultChunkSize) static let defaultChunkSize: Bytes = (6 * 1024 * 1024) @@ -36,11 +48,14 @@ extension Chunk { /// - file: The file for which the chunk should be created /// - chunkIndex: The index of the chunk /// - uploadURL: The URL to where the chunk should be uploaded - init(file: File, chunkIndex: Int, uploadURL: URL) { + init(file: File, chunkIndex: Int, uploadURL: URL) throws { + guard let fileURL = file.url else { + throw Error.noFileDataAvailable + } let chunkSize = file.chunkSize ?? Chunk.defaultChunkSize let byteOffset = chunkSize * Bytes(chunkIndex) self.init(chunkIndex: chunkIndex, - fileURL: file.url, + fileURL: fileURL, uploadURL: uploadURL, size: min(file.filesize - byteOffset, chunkSize), byteOffset: byteOffset) diff --git a/WeTransfer/Models/File.swift b/WeTransfer/Models/File.swift index 079abc4..ad506db 100644 --- a/WeTransfer/Models/File.swift +++ b/WeTransfer/Models/File.swift @@ -27,8 +27,8 @@ public final class File: Encodable { } } - /// Location of the file on disk - public let url: URL + /// Location of the file on disk. Files from remote boards no longer point to local files + public let url: URL? /// Server-side identifier when file is added to the transfer or board on the server public private(set) var identifier: String? @@ -37,9 +37,7 @@ public final class File: Encodable { public internal(set) var isUploaded: Bool = false /// Name of the file. Should be the last path component of the url - public var filename: String { - return url.lastPathComponent - } + public let filename: String /// Size of the file in Bytes public let filesize: Bytes @@ -51,6 +49,7 @@ public final class File: Encodable { private(set) var multipartUploadIdentifier: String? public init(url: URL) throws { + filename = url.lastPathComponent self.url = url let fileAttributes = try FileManager.default.attributesOfItem(atPath: url.path) diff --git a/WeTransfer/Server/Operations/CreateChunkOperation.swift b/WeTransfer/Server/Operations/CreateChunkOperation.swift index 0e93f40..b7629ff 100644 --- a/WeTransfer/Server/Operations/CreateChunkOperation.swift +++ b/WeTransfer/Server/Operations/CreateChunkOperation.swift @@ -68,9 +68,13 @@ final class CreateChunkOperation: AsynchronousResultOperation { guard let self = self else { return } - // Chunks are locally referenced in a zero-based index. Subtract 1 from partNumber value - let chunk = Chunk(file: self.file, chunkIndex: self.chunkIndex, uploadURL: response.url) - self.finish(with: .success(chunk)) + do { + // Chunks are locally referenced in a zero-based index. Subtract 1 from partNumber value + let chunk = try Chunk(file: self.file, chunkIndex: self.chunkIndex, uploadURL: response.url) + self.finish(with: .success(chunk)) + } catch { + self.finish(with: .failure(error)) + } } } } From 2f5cf69469682edf9a84a3d65a204e1fbba54703 Mon Sep 17 00:00:00 2001 From: Pim Coumans Date: Tue, 6 Nov 2018 15:25:58 +0100 Subject: [PATCH 5/5] Fixed test build error --- WeTransferTests/Transfer/TransferUploadTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WeTransferTests/Transfer/TransferUploadTests.swift b/WeTransferTests/Transfer/TransferUploadTests.swift index 6adb625..ed38c93 100644 --- a/WeTransferTests/Transfer/TransferUploadTests.swift +++ b/WeTransferTests/Transfer/TransferUploadTests.swift @@ -14,12 +14,12 @@ final class TransferUploadTests: BaseTestCase { var observation: NSKeyValueObservation? private func createTransfer(completion: @escaping (Transfer?) -> Void) { - guard let file = TestConfiguration.fileModel else { + guard let fileURL = TestConfiguration.imageFileURL else { XCTFail("File not available") return } - WeTransfer.createTransfer(saying: "TestTransfer", fileURLs: [file.url]) { result in + WeTransfer.createTransfer(saying: "TestTransfer", fileURLs: [fileURL]) { result in switch result { case .failure(let error): XCTFail("Transfer creation failed: \(error)")