A library that provides an Integrant key for managing objects in a FTP server. This library is a wrapper around miner/clj-ftp.
To use this library add the following key to your configuration:
:dev.gethop.object-storage/ftp
This key expects a configuration map with one unique mandatory key, plus other optional one. These are the mandatory keys:
:ftp-uri: FTP server URI to connect to.
These are the optional keys:
:ftp-options: a map of options for configuring the FTP client.- Since this library wraps around miner/clj-ftp all configuration options found there is also valid here.
Key initialization returns a FTP record that can be used to perform the FTP operations described below.
Basic configuration:
:dev.gethop.object-storage/ftp
{:ftp-uri #duct/env ["FTP_URI" Str :or "ftp://user:mypassword@my-ftp-server"]}Configuration with custom FTP client configuration:
:dev.gethop.object-storage/ftp
{:ftp-uri #duct/env ["FTP_URI" Str :or "ftp://user:mypassword@my-ftp-server"]
:ftp-options {:default-timeout-ms 30000
:security-mode :explicit
:local-data-connection-mode :active
:file-type :binary}]}If you are using the library as part of a Duct-based project, adding any of the previous configurations to your config.edn file will perform all the steps necessary to initialize the key and return a FTP record for the associated configuration. In order to show a few interactive usages of the library, we will do all the steps manually in the REPL.
First we require the relevant namespaces:
user> (require '[integrant.core :as ig]
'[dev.gethop.object-storage.core :as core])
nil
user>Next we create the configuration var holding the FTP integration configuration details:
user> (def config {:ftp-uri #duct/env ["FTP_URI" Str :or "ftp://user:mypassword@my-ftp-server"]})
#'user/config
user>Now that we have all pieces in place, we can initialize the :dev.gethop.object-storage/ftp Integrant key to get a FTP record. As we are doing all this from the REPL, we have to manually require dev.gethop.object-storage.ftp namespace, where the init-key multimethod for that key is defined (this is not needed when Duct takes care of initializing the key as part of the application start up):
user> (require '[dev.gethop.object-storage.ftp :as ftp])
nil
user>And we finally initialize the key with the configuration defined above, to get our FTP record:
user> (def ftp-record (->
config
(->> (ig/init-key :dev.gethop.object-storage/ftp))))
#'user/ftp-record
user> ftp-record
#dev.gethop.object-storage.ftp.FTP{{:ftp-uri "ftp://user:mypassword@my-ftp-server"
:ftp-options nil}
user>Now that we have our FTP record, we are ready to use the methods defined by the protocol ObjectStorage.
(get-object ftp-record object-id)
- description: Retrieves an object from a FTP server
- parameters:
ftp-record: aFTPrecordobject-id: the object
- return value: a map with the following keys
:success?: boolean stating if the operation was successful or not:object: If the operation was successful, this key contains an InputStream-compatible stream, on the desired object. Note that the InputStream returned by get-object should be closed (.e.g, via slurp).error-details: a map with additional details on the problem encountered while trying to retrieve the object.
Lets see an example. First a successful invocation:
user> (core/get-object ftp-record "test1.txt")
{:success? true,
:object #object[java.io.BufferedInputStream 0x7cc725c "java.io.BufferedInputStream@7cc725c"]}Invocation with a non-existing object-id:
user> (core/get-object ftp-record "i-dont-exist")
{:success? false}(put-object ftp-record object-id object)
- description: uploads an object to the FTP server with
object-idas its filename - parameters:
ftp-record: aFTPrecordobject-id: the object identifier in the FTP server in other words the filenameobject: the object we want to upload can be a file or an input stream.
- return value: a map with the following keys
:success?: boolean stating if the operation was successful or noterror-details: a map with additional details on the problem encountered while trying to retrieve the object.
Example:
user> (core/put-object ftp-record "test2.txt" (io/file "files/test2.txt"))
{:success? true}Invocation with a non-existing object:
user> (core/put-object ftp-record "test2.txt" (io/file "files/i-dont-exist.txt"))
{:success? false,
:error-details "files/i-dont-exist.txt (No such file or directory)"}(delete-object ftp-record object-id object)
- description: deletes an object with
object-idin the FTP server. - parameters:
ftp-record: aFTPrecordobject-id: the object identifier in the FTP server in other words the filename
- return value: a map with the following keys
:success?: boolean stating if the operation was successful or noterror-details: a map with additional details on the problem encountered while trying to retrieve the object.
Example:
user> (core/delete-object ftp-record "test2.txt")
{:success? true}Invocation with a non-existing object:
user> (core/delete-object ftp-record "i-dont-exist.txt")
{:success? false}(rename-object ftp-record object-id new-object-id)
- description: renames an object with
object-idto thenew-object-idin the FTP server. - parameters:
ftp-record: aFTPrecordobject-id: the object identifier in the FTP server in other words the filenamenew-object-id: the new object identifier
- return value: a map with the following keys
:success?: boolean stating if the operation was successful or noterror-details: a map with additional details on the problem encountered while trying to retrieve the object.
Example:
user> (core/rename-object ftp-record "test1.txt" "new-test1.txt")
{:success? true}Invocation with a non-existing object:
user> (core/rename-object ftp-record "i-dont-exist.txt" "new-i-dont-exist.txt")
{:success? false}(list-objects ftp-record parent-object-id)
- description: lists all objects under the
parent-object-id - parameters:
ftp-record: aFTPrecordparent-object-id: the identifier of a folder within the FTP server.
- return value: a map with the following keys
:success?: boolean stating if the operation was successful or not:objects: A collection of maps each representing an object with the following attributes:object-id: object's identifierlast-modified: an instant objectsize: size in bytestype: the type of the object that can be::file,:directory,:symbolic-linkor:unknown
error-details: a map with additional details on the problem encountered while trying to retrieve the object.
Example:
user> (core/list-objects ftp-record "")
{:success? true,
:objects
({:object-id "/files/folder-1/file-1",
:last-modified #inst "2019-12-05T19:06:00.000+01:00",
:size 15,
:type :file}
{:object-id "/files/folder-1/folder-1-1",
:last-modified #inst "2020-02-28T10:13:00.000+01:00",
:size 4096,
:type :directory}
{:object-id "/files/folder-1/folder-1-1/file-2",
:last-modified #inst "2020-02-28T10:13:00.000+01:00",
:size 6,
:type :file}
{:object-id "/files/folder-1/folder-1-1/file-3",
:last-modified #inst "2020-02-28T09:52:00.000+01:00",
:size 8,
:type :file})}The library includes self-contained units tests, including some integration tests that depend on a FTP server. Those tests have the ^:integration metadata keyword associated to them, so you can exclude them from our unit tests runs.
If you want to run the integration tests, the following environment variable is needed:
TEST_OBJECT_STORAGE_FTP_URI: The uri of the FTP server used the integration tests. Be aware that the tests will leave trash files in the server. It may corrupt or delete files, so don't execute it against a real server.
Copyright (c) 2024 Biotz, SL.
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/