Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"dependencies": {},
"devDependencies": {
"@types/node": "^20.14.8",
"typescript": "~5.4.5"
"@types/node": "^24.10.1",
"typescript": "^5.9.3"
}
}
2 changes: 0 additions & 2 deletions packages/capacitor-plugin/.eslintignore

This file was deleted.

2 changes: 0 additions & 2 deletions packages/capacitor-plugin/.prettierignore

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
*/
public class ExampleUnitTest {

@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
52 changes: 52 additions & 0 deletions packages/capacitor-plugin/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import eslintjs from "@eslint/js";
import tseslint from "@typescript-eslint/eslint-plugin";
import tseslintparser from "@typescript-eslint/parser";
import prettierConfig from "eslint-config-prettier";
import prettierPlugin from "eslint-plugin-prettier";

export default [
{
ignores: ["node_modules/**", "dist/**", "build/**", "eslint.config.*"],
},
eslintjs.configs.recommended,
{
files: ["**/*.ts", "**/*.js"],
languageOptions: {
parser: tseslintparser,
ecmaVersion: "latest",
sourceType: "module",
globals: {
window: "readonly",
document: "readonly",
console: "readonly",
Blob: "readonly",
Headers: "readonly",
RequestInit: "readonly",
AbortController: "readonly",
setTimeout: "readonly",
clearTimeout: "readonly",
fetch: "readonly",
URL: "readonly",
XMLHttpRequest: "readonly",
FormData: "readonly",
atob: "readonly",
btoa: "readonly",
FileReader: "readonly",
},
},
plugins: {
"@typescript-eslint": tseslint,
prettier: prettierPlugin,
},
rules: {
...tseslint.configs.recommended.rules,
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_" },
],
"prettier/prettier": "error",
},
},
prettierConfig,
];
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import IONFileTransferLib
/// `FileTransferError` represents various error states that can occur during file uploads and downloads,
/// including validation issues, connection problems, HTTP response errors, and file system errors.
struct FileTransferError: Error {

/// A error code in the format `OS-PLUG-FLTR-XXXX`.
let code: String

/// A human-readable error message.
let message: String

/// The source URL or path related to the error, if available.
var source: String?

Expand Down Expand Up @@ -62,7 +62,7 @@ struct FileTransferError: Error {
self.headers = headers
self.cause = cause
}

/// A dictionary representation of the error for use in JavaScript or other serialization contexts.
///
/// This includes the code, message, and optional metadata such as HTTP status,
Expand All @@ -89,11 +89,11 @@ struct FileTransferError: Error {
// MARK: - Static Constructors

extension FileTransferError {

static func invalidParameters(_ message: String? = nil) -> FileTransferError {
.init(code: 4, message: message ?? "The method's input parameters aren't valid.")
}

static func invalidServerUrl(_ url: String?) -> FileTransferError {
.init(
code: 5,
Expand All @@ -103,23 +103,23 @@ extension FileTransferError {
source: url
)
}

static func fileDoesNotExist() -> FileTransferError {
.init(code: 7, message: "Operation failed because file does not exist.")
}

static func connectionError() -> FileTransferError {
.init(code: 8, message: "Failed to connect to server.")
}

static func notModified() -> FileTransferError {
.init(
code: 9,
message: "The server responded with HTTP 304 – Not Modified. If you want to avoid this, check your headers related to HTTP caching.",
httpStatus: 304
)
}

static func httpError(
responseCode: Int,
message: String,
Expand All @@ -136,7 +136,7 @@ extension FileTransferError {
cause: cause
)
}

static func genericError(
cause: Error? = nil
) -> FileTransferError {
Expand All @@ -151,7 +151,7 @@ extension FileTransferError {
// MARK: - IONFLTRException Mapping

extension IONFLTRException {

/// Converts an `IONFLTRException` to a corresponding `FileTransferError`.
///
/// This method maps specific cases of `IONFLTRException` to their
Expand All @@ -173,14 +173,14 @@ extension IONFLTRException {
return FileTransferError.genericError(cause: self)
case .httpError(let responseCode, let responseBody, let headers):
return responseCode == 304
? FileTransferError.notModified()
: FileTransferError.httpError(
responseCode: responseCode,
message: self.description,
responseBody: responseBody,
headers: headers,
cause: self
)
? FileTransferError.notModified()
: FileTransferError.httpError(
responseCode: responseCode,
message: self.description,
responseBody: responseBody,
headers: headers,
cause: self
)
case .connectionError:
return FileTransferError.connectionError()
case .transferError:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,33 +31,33 @@ public class FileTransferPlugin: CAPPlugin, CAPBridgedPlugin {
/// - Parameter call: The Capacitor call containing `url`, `path`, and optional HTTP options.
@objc func downloadFile(_ call: CAPPluginCall) {
do {
let (serverURL, fileURL, shouldTrackProgress, httpOptions) = try validateAndPrepare(call: call, action: .download)
let prepData = try validateAndPrepare(call: call, action: .download)

try manager.downloadFile(
fromServerURL: serverURL,
toFileURL: fileURL,
withHttpOptions: httpOptions
fromServerURL: prepData.serverURL,
toFileURL: prepData.fileURL,
withHttpOptions: prepData.httpOptions
).sink(
receiveCompletion: handleCompletion(call: call, source: serverURL.absoluteString, target: fileURL.absoluteString),
receiveCompletion: handleCompletion(call: call, source: prepData.serverURL.absoluteString, target: prepData.fileURL.absoluteString),
receiveValue: handleReceiveValue(
call: call,
type: .download,
url: serverURL.absoluteString,
path: fileURL.path,
shouldTrackProgress: shouldTrackProgress
url: prepData.serverURL.absoluteString,
path: prepData.fileURL.path,
shouldTrackProgress: prepData.shouldTrackProgress
)
).store(in: &cancellables)
} catch {
call.sendError(error, source: call.getString("url"), target: call.getString("path"))
}
}

/// Uploads a file from the provided path to the specified server URL.
///
/// - Parameter call: The Capacitor call containing `url`, `path`, `fileKey`, and optional HTTP options.
@objc func uploadFile(_ call: CAPPluginCall) {
do {
let (serverURL, fileURL, shouldTrackProgress, httpOptions) = try validateAndPrepare(call: call, action: .upload)
let prepData = try validateAndPrepare(call: call, action: .upload)
let chunkedMode = call.getBool("chunkedMode", false)
let mimeType = call.getString("mimeType")
let fileKey = call.getString("fileKey") ?? "file"
Expand All @@ -66,55 +66,63 @@ public class FileTransferPlugin: CAPPlugin, CAPBridgedPlugin {
mimeType: mimeType,
fileKey: fileKey
)

try manager.uploadFile(
fromFileURL: fileURL,
toServerURL: serverURL,
fromFileURL: prepData.fileURL,
toServerURL: prepData.serverURL,
withUploadOptions: uploadOptions,
andHttpOptions: httpOptions
andHttpOptions: prepData.httpOptions
).sink(
receiveCompletion: handleCompletion(call: call, source: fileURL.absoluteString, target: serverURL.absoluteString),
receiveCompletion: handleCompletion(call: call, source: prepData.fileURL.absoluteString, target: prepData.serverURL.absoluteString),
receiveValue: handleReceiveValue(
call: call,
type: .upload,
url: serverURL.absoluteString,
path: fileURL.path,
shouldTrackProgress: shouldTrackProgress
url: prepData.serverURL.absoluteString,
path: prepData.fileURL.path,
shouldTrackProgress: prepData.shouldTrackProgress
)
).store(in: &cancellables)
} catch {
call.sendError(error, source: call.getString("path"), target: call.getString("url"))
}
}


/// Structure to hold transfer preparation data.
private struct TransferPreparationData {
let serverURL: URL
let fileURL: URL
let shouldTrackProgress: Bool
let httpOptions: IONFLTRHttpOptions
}

/// Validates parameters from the call and prepares transfer-related data.
///
/// - Parameters:
/// - call: The plugin call.
/// - action: The type of action (`upload` or `download`).
/// - Throws: An error if validation fails.
/// - Returns: Tuple containing server URL, file URL, progress flag, and HTTP options.
private func validateAndPrepare(call: CAPPluginCall, action: Action) throws -> (URL, URL, Bool, IONFLTRHttpOptions) {
/// - Returns: Structure containing server URL, file URL, progress flag, and HTTP options.
private func validateAndPrepare(call: CAPPluginCall, action: Action) throws -> TransferPreparationData {
guard let url = call.getString("url") else {
throw FileTransferError.invalidServerUrl(nil)
}

guard let serverURL = URL(string: url) else {
throw FileTransferError.invalidServerUrl(url)
}

guard let path = call.getString("path") else {
throw FileTransferError.invalidParameters("Path is required.")
}

guard let fileURL = URL(string: path) else {
throw FileTransferError.invalidParameters("Path is invalid.")
}

let shouldTrackProgress = call.getBool("progress", false)
let headers = call.getObject("headers") ?? JSObject()
let params = call.getObject("params") ?? JSObject()

let httpOptions = IONFLTRHttpOptions(
method: call.getString("method") ?? defaultHTTPMethod(for: action),
params: extractParams(from: params),
Expand All @@ -123,10 +131,15 @@ public class FileTransferPlugin: CAPPlugin, CAPBridgedPlugin {
disableRedirects: call.getBool("disableRedirects", false),
shouldEncodeUrlParams: call.getBool("shouldEncodeUrlParams", true)
)

return (serverURL, fileURL, shouldTrackProgress, httpOptions)

return TransferPreparationData(
serverURL: serverURL,
fileURL: fileURL,
shouldTrackProgress: shouldTrackProgress,
httpOptions: httpOptions
)
}

/// Provides the default HTTP method for the given action.
private func defaultHTTPMethod(for action: Action) -> String {
switch action {
Expand All @@ -136,7 +149,7 @@ public class FileTransferPlugin: CAPPlugin, CAPBridgedPlugin {
return "POST"
}
}

/// Converts a JSObject to a string dictionary used for headers.
private func extractHeaders(from jsObject: JSObject) -> [String: String] {
return jsObject.reduce(into: [String: String]()) { result, pair in
Expand All @@ -161,7 +174,7 @@ public class FileTransferPlugin: CAPPlugin, CAPBridgedPlugin {
}
return result
}

/// Handles completion of the upload or download Combine pipeline.
private func handleCompletion(call: CAPPluginCall, source: String, target: String) -> (Subscribers.Completion<Error>) -> Void {
return { completion in
Expand Down Expand Up @@ -228,7 +241,7 @@ public class FileTransferPlugin: CAPPlugin, CAPBridgedPlugin {
}
}
}

/// Reports a progress event to JavaScript listeners if conditions are met.
///
/// This method emits a `"progress"` event with details about the transfer status, including
Expand Down Expand Up @@ -274,12 +287,12 @@ extension CAPPluginCall {
func sendError(_ error: Error, source: String?, target: String?) {
var pluginError: FileTransferError
switch error {
case let error as FileTransferError:
pluginError = error
case let error as IONFLTRException:
pluginError = error.toFileTransferError()
default:
pluginError = .genericError(cause: error)
case let error as FileTransferError:
pluginError = error
case let error as IONFLTRException:
pluginError = error.toFileTransferError()
default:
pluginError = .genericError(cause: error)
}
pluginError.source = source
pluginError.target = target
Expand Down
Loading