embulk-output-sftp 0.0.9 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +3 -3
- data/build.gradle +68 -7
- data/classpath/commons-io-2.5.jar +0 -0
- data/classpath/embulk-output-sftp-0.1.1.jar +0 -0
- data/src/main/java/org/embulk/output/sftp/SftpFileOutput.java +84 -116
- metadata +19 -16
- data/classpath/embulk-output-sftp-0.0.9.jar +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 831b167ddd205c7ad62b1306a47c634eb7c52aa5
|
4
|
+
data.tar.gz: 2de997335de69c887b58507cd820be9593a06483
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cfa0bf45de009ad868e66ded095e693f53aa3a5db03d166e6460ec686668e09018106868b055bf2fbe7aaa18522beb80701de7a85768910fcbb4bfa6331860e7
|
7
|
+
data.tar.gz: e7ea2ee48cc39c457a57dc368ac87918697aa85c1e372fe64a7740cbaccc360ec0ce10814bad6b7029bf51799b610b7d2c441a26a38edf7b8aeb141f35bee1d1
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
#
|
2
|
-
[](https://coveralls.io/github/civitaspo/embulk-output-sftp?branch=master)
|
1
|
+
# SFTP file output plugin for Embulk
|
2
|
+
[](https://travis-ci.org/embulk/embulk-output-sftp)
|
4
3
|
|
5
4
|
Stores files on a SFTP Server
|
6
5
|
|
@@ -125,6 +124,7 @@ $ embulk run -Ilib example/sample.yml
|
|
125
124
|
|
126
125
|
```
|
127
126
|
$ ./gradlew gem # -t to watch change of files and rebuild continuously
|
127
|
+
$ ./gradlew bintrayUpload # release embulk-output-sftp to Bintray maven repo
|
128
128
|
```
|
129
129
|
|
130
130
|
## Note
|
data/build.gradle
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
plugins {
|
2
|
-
id "com.jfrog.bintray" version "1.
|
2
|
+
id "com.jfrog.bintray" version "1.7"
|
3
|
+
id "maven-publish"
|
3
4
|
id "com.github.jruby-gradle.base" version "0.1.5"
|
4
5
|
id "com.github.kt3k.coveralls" version "2.4.0"
|
5
6
|
id "jacoco"
|
@@ -15,7 +16,8 @@ configurations {
|
|
15
16
|
provided
|
16
17
|
}
|
17
18
|
|
18
|
-
|
19
|
+
group = "org.embulk.output.sftp"
|
20
|
+
version = "0.1.1"
|
19
21
|
sourceCompatibility = 1.7
|
20
22
|
targetCompatibility = 1.7
|
21
23
|
|
@@ -24,6 +26,7 @@ dependencies {
|
|
24
26
|
provided "org.embulk:embulk-core:0.8.6"
|
25
27
|
// compile "YOUR_JAR_DEPENDENCY_GROUP:YOUR_JAR_DEPENDENCY_MODULE:YOUR_JAR_DEPENDENCY_VERSION"
|
26
28
|
compile "org.apache.commons:commons-vfs2:2.1.1660580.2"
|
29
|
+
compile "commons-io:commons-io:2.5"
|
27
30
|
compile "com.jcraft:jsch:0.1.53"
|
28
31
|
testCompile "junit:junit:4.+"
|
29
32
|
testCompile "org.embulk:embulk-core:0.8.6:tests"
|
@@ -40,6 +43,50 @@ jacocoTestReport {
|
|
40
43
|
}
|
41
44
|
}
|
42
45
|
|
46
|
+
javadoc {
|
47
|
+
options {
|
48
|
+
locale = 'en_US'
|
49
|
+
encoding = 'UTF-8'
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
// bintray
|
54
|
+
bintray {
|
55
|
+
// write at your bintray user name and api key to ~/.gradle/gradle.properties file:
|
56
|
+
user = project.hasProperty('bintray_user') ? bintray_user : ''
|
57
|
+
key = project.hasProperty('bintray_api_key') ? bintray_api_key : ''
|
58
|
+
|
59
|
+
publications = ['bintrayMavenRelease']
|
60
|
+
publish = true
|
61
|
+
|
62
|
+
pkg {
|
63
|
+
userOrg = 'embulk-output-sftp'
|
64
|
+
repo = 'maven'
|
65
|
+
name = project.name
|
66
|
+
desc = 'SFTP output plugin for Embulk'
|
67
|
+
websiteUrl = 'https://github.com/embulk/embulk-output-sftp'
|
68
|
+
issueTrackerUrl = 'https://github.com/embulk/embulk-output-sftp/issues'
|
69
|
+
vcsUrl = 'https://github.com/embulk/embulk-output-sftp.git'
|
70
|
+
licenses = ['MIT']
|
71
|
+
labels = ['embulk', 'java']
|
72
|
+
publicDownloadNumbers = true
|
73
|
+
|
74
|
+
version {
|
75
|
+
name = project.version
|
76
|
+
}
|
77
|
+
}
|
78
|
+
}
|
79
|
+
publishing {
|
80
|
+
publications {
|
81
|
+
bintrayMavenRelease(MavenPublication) {
|
82
|
+
from components.java
|
83
|
+
artifact testsJar
|
84
|
+
artifact sourcesJar
|
85
|
+
artifact javadocJar
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
43
90
|
task classpath(type: Copy, dependsOn: ["jar"]) {
|
44
91
|
doFirst { file("classpath").deleteDir() }
|
45
92
|
from (configurations.runtime - configurations.provided + files(jar.archivePath))
|
@@ -47,6 +94,20 @@ task classpath(type: Copy, dependsOn: ["jar"]) {
|
|
47
94
|
}
|
48
95
|
clean { delete "classpath" }
|
49
96
|
|
97
|
+
// add tests/javadoc/source jar tasks as artifacts to be released
|
98
|
+
task testsJar(type: Jar, dependsOn: classes) {
|
99
|
+
classifier = 'tests'
|
100
|
+
from sourceSets.test.output
|
101
|
+
}
|
102
|
+
task sourcesJar(type: Jar, dependsOn: classes) {
|
103
|
+
classifier = 'sources'
|
104
|
+
from sourceSets.main.allSource
|
105
|
+
}
|
106
|
+
task javadocJar(type: Jar, dependsOn: javadoc) {
|
107
|
+
classifier = 'javadoc'
|
108
|
+
from javadoc.destinationDir
|
109
|
+
}
|
110
|
+
|
50
111
|
checkstyle {
|
51
112
|
configFile = file("${project.rootDir}/config/checkstyle/checkstyle.xml")
|
52
113
|
toolVersion = '6.14.1'
|
@@ -88,12 +149,12 @@ task gemspec {
|
|
88
149
|
Gem::Specification.new do |spec|
|
89
150
|
spec.name = "${project.name}"
|
90
151
|
spec.version = "${project.version}"
|
91
|
-
spec.authors = ["Civitaspo"]
|
92
|
-
spec.summary = %[
|
93
|
-
spec.description = %[Stores files on
|
94
|
-
spec.email = ["civitaspo@gmail.com"]
|
152
|
+
spec.authors = ["Civitaspo", "Satoshi Akama"]
|
153
|
+
spec.summary = %[SFTP file output plugin for Embulk]
|
154
|
+
spec.description = %[Stores files on SFTP server.]
|
155
|
+
spec.email = ["civitaspo@gmail.com", "satoshiakama@gmail.com"]
|
95
156
|
spec.licenses = ["MIT"]
|
96
|
-
spec.homepage = "https://github.com/
|
157
|
+
spec.homepage = "https://github.com/embulk/embulk-output-sftp"
|
97
158
|
|
98
159
|
spec.files = `git ls-files`.split("\n") + Dir["classpath/*.jar"]
|
99
160
|
spec.test_files = spec.files.grep(%r"^(test|spec)/")
|
Binary file
|
Binary file
|
@@ -2,6 +2,7 @@ package org.embulk.output.sftp;
|
|
2
2
|
|
3
3
|
import com.google.common.base.Function;
|
4
4
|
import com.google.common.base.Throwables;
|
5
|
+
import org.apache.commons.io.IOUtils;
|
5
6
|
import org.apache.commons.vfs2.FileObject;
|
6
7
|
import org.apache.commons.vfs2.FileSystemException;
|
7
8
|
import org.apache.commons.vfs2.FileSystemOptions;
|
@@ -15,15 +16,22 @@ import org.embulk.spi.Exec;
|
|
15
16
|
import org.embulk.spi.FileOutput;
|
16
17
|
import org.embulk.spi.TransactionalFileOutput;
|
17
18
|
import org.embulk.spi.unit.LocalFile;
|
19
|
+
import org.embulk.spi.util.RetryExecutor.RetryGiveupException;
|
20
|
+
import org.embulk.spi.util.RetryExecutor.Retryable;
|
18
21
|
import org.slf4j.Logger;
|
19
22
|
|
23
|
+
import java.io.BufferedInputStream;
|
24
|
+
import java.io.BufferedOutputStream;
|
20
25
|
import java.io.File;
|
26
|
+
import java.io.FileInputStream;
|
27
|
+
import java.io.FileNotFoundException;
|
28
|
+
import java.io.FileOutputStream;
|
21
29
|
import java.io.IOException;
|
22
|
-
import java.io.OutputStream;
|
23
30
|
import java.net.URI;
|
24
31
|
import java.net.URISyntaxException;
|
25
32
|
|
26
33
|
import static org.embulk.output.sftp.SftpFileOutputPlugin.PluginTask;
|
34
|
+
import static org.embulk.spi.util.RetryExecutor.retryExecutor;
|
27
35
|
|
28
36
|
/**
|
29
37
|
* Created by takahiro.nakayama on 10/20/15.
|
@@ -44,8 +52,8 @@ public class SftpFileOutput
|
|
44
52
|
|
45
53
|
private final int taskIndex;
|
46
54
|
private int fileIndex = 0;
|
47
|
-
private
|
48
|
-
private
|
55
|
+
private File tempFile;
|
56
|
+
private BufferedOutputStream localOutput = null;
|
49
57
|
|
50
58
|
private StandardFileSystemManager initializeStandardFileSystemManager()
|
51
59
|
{
|
@@ -144,11 +152,10 @@ public class SftpFileOutput
|
|
144
152
|
closeCurrentFile();
|
145
153
|
|
146
154
|
try {
|
147
|
-
|
148
|
-
|
149
|
-
logger.info("new sftp file: {}", currentFile.getPublicURIString());
|
155
|
+
tempFile = Exec.getTempFileSpace().createTempFile();
|
156
|
+
localOutput = new BufferedOutputStream(new FileOutputStream(tempFile));
|
150
157
|
}
|
151
|
-
catch (
|
158
|
+
catch (FileNotFoundException e) {
|
152
159
|
logger.error(e.getMessage());
|
153
160
|
Throwables.propagate(e);
|
154
161
|
}
|
@@ -157,28 +164,11 @@ public class SftpFileOutput
|
|
157
164
|
@Override
|
158
165
|
public void add(final Buffer buffer)
|
159
166
|
{
|
160
|
-
if (currentFile == null) {
|
161
|
-
throw new IllegalStateException("nextFile() must be called before poll()");
|
162
|
-
}
|
163
|
-
|
164
167
|
try {
|
165
|
-
|
166
|
-
public Void execute() throws IOException
|
167
|
-
{
|
168
|
-
currentFileOutputStream.write(buffer.array(), buffer.offset(), buffer.limit());
|
169
|
-
return null;
|
170
|
-
}
|
171
|
-
};
|
172
|
-
try {
|
173
|
-
withConnectionRetry(retriable);
|
174
|
-
}
|
175
|
-
catch (Exception e) {
|
176
|
-
throw (IOException) e;
|
177
|
-
}
|
168
|
+
localOutput.write(buffer.array(), buffer.offset(), buffer.limit());
|
178
169
|
}
|
179
|
-
catch (IOException
|
180
|
-
|
181
|
-
Throwables.propagate(e);
|
170
|
+
catch (IOException ex) {
|
171
|
+
throw Throwables.propagate(ex);
|
182
172
|
}
|
183
173
|
finally {
|
184
174
|
buffer.release();
|
@@ -189,6 +179,7 @@ public class SftpFileOutput
|
|
189
179
|
public void finish()
|
190
180
|
{
|
191
181
|
closeCurrentFile();
|
182
|
+
uploadFile(getOutputFilePath());
|
192
183
|
}
|
193
184
|
|
194
185
|
@Override
|
@@ -211,27 +202,68 @@ public class SftpFileOutput
|
|
211
202
|
|
212
203
|
private void closeCurrentFile()
|
213
204
|
{
|
214
|
-
if (
|
215
|
-
|
205
|
+
if (localOutput != null) {
|
206
|
+
try {
|
207
|
+
localOutput.close();
|
208
|
+
}
|
209
|
+
catch (IOException ex) {
|
210
|
+
throw Throwables.propagate(ex);
|
211
|
+
}
|
216
212
|
}
|
213
|
+
}
|
217
214
|
|
215
|
+
private Void uploadFile(final String remotePath)
|
216
|
+
{
|
218
217
|
try {
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
218
|
+
return retryExecutor()
|
219
|
+
.withRetryLimit(maxConnectionRetry)
|
220
|
+
.withInitialRetryWait(500)
|
221
|
+
.withMaxRetryWait(30 * 1000)
|
222
|
+
.runInterruptible(new Retryable<Void>() {
|
223
|
+
@Override
|
224
|
+
public Void call() throws IOException
|
225
|
+
{
|
226
|
+
FileObject remoteFile = newSftpFile(getSftpFileUri(remotePath));
|
227
|
+
logger.info("new sftp file: {}", remoteFile.getPublicURIString());
|
228
|
+
try (BufferedOutputStream outputStream = new BufferedOutputStream(remoteFile.getContent().getOutputStream())) {
|
229
|
+
try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(tempFile))) {
|
230
|
+
IOUtils.copy(inputStream, outputStream);
|
231
|
+
}
|
232
|
+
}
|
233
|
+
return null;
|
234
|
+
}
|
235
|
+
|
236
|
+
@Override
|
237
|
+
public boolean isRetryableException(Exception exception)
|
238
|
+
{
|
239
|
+
return true;
|
240
|
+
}
|
241
|
+
|
242
|
+
@Override
|
243
|
+
public void onRetry(Exception exception, int retryCount, int retryLimit, int retryWait) throws RetryGiveupException
|
244
|
+
{
|
245
|
+
String message = String.format("SFTP output failed. Retrying %d/%d after %d seconds. Message: %s",
|
246
|
+
retryCount, retryLimit, retryWait / 1000, exception.getMessage());
|
247
|
+
if (retryCount % 3 == 0) {
|
248
|
+
logger.warn(message, exception);
|
249
|
+
}
|
250
|
+
else {
|
251
|
+
logger.warn(message);
|
252
|
+
}
|
253
|
+
}
|
254
|
+
|
255
|
+
@Override
|
256
|
+
public void onGiveup(Exception firstException, Exception lastException) throws RetryGiveupException
|
257
|
+
{
|
258
|
+
}
|
259
|
+
});
|
223
260
|
}
|
224
|
-
|
225
|
-
|
226
|
-
currentFile.close();
|
261
|
+
catch (RetryGiveupException ex) {
|
262
|
+
throw Throwables.propagate(ex.getCause());
|
227
263
|
}
|
228
|
-
catch (
|
229
|
-
|
264
|
+
catch (InterruptedException ex) {
|
265
|
+
throw Throwables.propagate(ex);
|
230
266
|
}
|
231
|
-
|
232
|
-
fileIndex++;
|
233
|
-
currentFile = null;
|
234
|
-
currentFileOutputStream = null;
|
235
267
|
}
|
236
268
|
|
237
269
|
private URI getSftpFileUri(String remoteFilePath)
|
@@ -250,84 +282,20 @@ public class SftpFileOutput
|
|
250
282
|
return pathPrefix + String.format(sequenceFormat, taskIndex, fileIndex) + fileNameExtension;
|
251
283
|
}
|
252
284
|
|
253
|
-
|
254
|
-
{
|
255
|
-
/**
|
256
|
-
* Execute the operation with the given (or null) return value.
|
257
|
-
* @return any return value from the operation
|
258
|
-
* @throws Exception
|
259
|
-
*/
|
260
|
-
public T execute() throws Exception;
|
261
|
-
}
|
262
|
-
|
263
|
-
private <T> T withConnectionRetry(final Retriable<T> op)
|
264
|
-
throws Exception
|
285
|
+
private FileObject newSftpFile(final URI sftpUri) throws FileSystemException
|
265
286
|
{
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
return op.execute();
|
270
|
-
}
|
271
|
-
catch (final Exception e) {
|
272
|
-
if (++count > maxConnectionRetry) {
|
273
|
-
throw e;
|
274
|
-
}
|
275
|
-
logger.warn("failed to connect sftp server: " + e.getMessage(), e);
|
276
|
-
|
277
|
-
try {
|
278
|
-
long sleepTime = ((long) Math.pow(2, count) * 1000);
|
279
|
-
logger.warn("sleep in next connection retry: {} milliseconds", sleepTime);
|
280
|
-
Thread.sleep(sleepTime); // milliseconds
|
281
|
-
}
|
282
|
-
catch (InterruptedException e1) {
|
283
|
-
// Ignore this exception because this exception is just about `sleep`.
|
284
|
-
logger.warn(e1.getMessage(), e1);
|
285
|
-
}
|
286
|
-
logger.warn("retry to connect sftp server: " + count + " times");
|
287
|
-
}
|
287
|
+
FileObject file = manager.resolveFile(sftpUri.toString(), fsOptions);
|
288
|
+
if (file.exists()) {
|
289
|
+
file.delete();
|
288
290
|
}
|
289
|
-
|
290
|
-
|
291
|
-
private FileObject newSftpFile(final URI sftpUri)
|
292
|
-
throws FileSystemException
|
293
|
-
{
|
294
|
-
Retriable<FileObject> retriable = new Retriable<FileObject>() {
|
295
|
-
public FileObject execute() throws FileSystemException
|
296
|
-
{
|
297
|
-
FileObject file = manager.resolveFile(sftpUri.toString(), fsOptions);
|
298
|
-
if (file.getParent().exists()) {
|
299
|
-
logger.info("parent directory {} exists there", file.getParent().getPublicURIString());
|
300
|
-
}
|
301
|
-
else {
|
302
|
-
logger.info("trying to create parent directory {}", file.getParent().getPublicURIString());
|
303
|
-
file.getParent().createFolder();
|
304
|
-
}
|
305
|
-
return file;
|
306
|
-
}
|
307
|
-
};
|
308
|
-
try {
|
309
|
-
return withConnectionRetry(retriable);
|
310
|
-
}
|
311
|
-
catch (Exception e) {
|
312
|
-
throw (FileSystemException) e;
|
313
|
-
}
|
314
|
-
}
|
315
|
-
|
316
|
-
private OutputStream newSftpOutputStream(final FileObject file)
|
317
|
-
throws FileSystemException
|
318
|
-
{
|
319
|
-
Retriable<OutputStream> retriable = new Retriable<OutputStream>() {
|
320
|
-
public OutputStream execute() throws FileSystemException
|
321
|
-
{
|
322
|
-
return file.getContent().getOutputStream();
|
323
|
-
}
|
324
|
-
};
|
325
|
-
try {
|
326
|
-
return withConnectionRetry(retriable);
|
291
|
+
if (file.getParent().exists()) {
|
292
|
+
logger.info("parent directory {} exists there", file.getParent().getPublicURIString());
|
327
293
|
}
|
328
|
-
|
329
|
-
|
294
|
+
else {
|
295
|
+
logger.info("trying to create parent directory {}", file.getParent().getPublicURIString());
|
296
|
+
file.getParent().createFolder();
|
330
297
|
}
|
298
|
+
return file;
|
331
299
|
}
|
332
300
|
|
333
301
|
private Function<LocalFile, String> localFileToPathString()
|
metadata
CHANGED
@@ -1,46 +1,48 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: embulk-output-sftp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Civitaspo
|
8
|
+
- Satoshi Akama
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2017-
|
12
|
+
date: 2017-05-29 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
|
-
|
15
|
+
name: bundler
|
16
|
+
version_requirements: !ruby/object:Gem::Requirement
|
15
17
|
requirements:
|
16
18
|
- - ~>
|
17
19
|
- !ruby/object:Gem::Version
|
18
20
|
version: '1.0'
|
19
|
-
|
20
|
-
prerelease: false
|
21
|
-
type: :development
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
21
|
+
requirement: !ruby/object:Gem::Requirement
|
23
22
|
requirements:
|
24
23
|
- - ~>
|
25
24
|
- !ruby/object:Gem::Version
|
26
25
|
version: '1.0'
|
26
|
+
prerelease: false
|
27
|
+
type: :development
|
27
28
|
- !ruby/object:Gem::Dependency
|
28
|
-
|
29
|
+
name: rake
|
30
|
+
version_requirements: !ruby/object:Gem::Requirement
|
29
31
|
requirements:
|
30
32
|
- - '>='
|
31
33
|
- !ruby/object:Gem::Version
|
32
34
|
version: '10.0'
|
33
|
-
|
34
|
-
prerelease: false
|
35
|
-
type: :development
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
37
36
|
requirements:
|
38
37
|
- - '>='
|
39
38
|
- !ruby/object:Gem::Version
|
40
39
|
version: '10.0'
|
41
|
-
|
40
|
+
prerelease: false
|
41
|
+
type: :development
|
42
|
+
description: Stores files on SFTP server.
|
42
43
|
email:
|
43
44
|
- civitaspo@gmail.com
|
45
|
+
- satoshiakama@gmail.com
|
44
46
|
executables: []
|
45
47
|
extensions: []
|
46
48
|
extra_rdoc_files: []
|
@@ -67,11 +69,12 @@ files:
|
|
67
69
|
- src/test/java/org/embulk/output/sftp/TestSftpFileOutputPlugin.java
|
68
70
|
- src/test/resources/id_rsa
|
69
71
|
- src/test/resources/id_rsa.pub
|
72
|
+
- classpath/commons-io-2.5.jar
|
70
73
|
- classpath/commons-logging-1.2.jar
|
71
74
|
- classpath/commons-vfs2-2.1.1660580.2.jar
|
72
|
-
- classpath/embulk-output-sftp-0.
|
75
|
+
- classpath/embulk-output-sftp-0.1.1.jar
|
73
76
|
- classpath/jsch-0.1.53.jar
|
74
|
-
homepage: https://github.com/
|
77
|
+
homepage: https://github.com/embulk/embulk-output-sftp
|
75
78
|
licenses:
|
76
79
|
- MIT
|
77
80
|
metadata: {}
|
@@ -94,5 +97,5 @@ rubyforge_project:
|
|
94
97
|
rubygems_version: 2.1.9
|
95
98
|
signing_key:
|
96
99
|
specification_version: 4
|
97
|
-
summary:
|
100
|
+
summary: SFTP file output plugin for Embulk
|
98
101
|
test_files: []
|
Binary file
|