Browse code

[cli] Introduce bm-cli user import

Arnaud Aujon Chevallier authored on 30/04/2019 06:37:39
Showing 8 changed files
... ...
@@ -18,14 +18,8 @@
18 18
 package net.bluemind.cli.calendar;
19 19
 
20 20
 import java.io.File;
21
-import java.io.IOException;
22
-import java.io.InputStream;
23
-import java.nio.file.Files;
24
-import java.nio.file.Paths;
25 21
 import java.util.Optional;
26 22
 
27
-import org.vertx.java.core.buffer.Buffer;
28
-
29 23
 import io.airlift.airline.Arguments;
30 24
 import io.airlift.airline.Command;
31 25
 import io.airlift.airline.Option;
... ...
@@ -37,9 +31,6 @@ import net.bluemind.cli.cmd.api.ICmdLet;
37 31
 import net.bluemind.cli.cmd.api.ICmdLetRegistration;
38 32
 import net.bluemind.cli.utils.CliUtils;
39 33
 import net.bluemind.core.api.Regex;
40
-import net.bluemind.core.api.Stream;
41
-import net.bluemind.core.rest.base.GenericStream;
42
-import net.bluemind.core.rest.vertx.VertxStream;
43 34
 
44 35
 @Command(name = "import", description = "import an ICS File")
45 36
 public class ImportCalendarCommand implements ICmdLet, Runnable {
... ...
@@ -90,52 +81,19 @@ public class ImportCalendarCommand implements ICmdLet, Runnable {
90 81
 
91 82
 		File file = new File(icsFilePath);
92 83
 		if (!file.exists() || file.isDirectory()) {
93
-			throw new CliException("File " + icsFilePath + " already exist.");
84
+			throw new CliException("File " + icsFilePath + " doesn't exist.");
94 85
 		}
95 86
 
96 87
 		if (calendarUid == null) {
97 88
 			calendarUid = CalendarContainerType.defaultUserCalendar(userUid);
98 89
 		}
99 90
 
100
-		try {
101
-			if (!dry) {
102
-				ctx.adminApi().instance(IVEvent.class, calendarUid).importIcs(getIcsFromFile(icsFilePath));
103
-				ctx.info("calendar " + calendarUid + " of " + email + " was imported");
104
-			} else {
105
-				ctx.info("DRY : calendar " + calendarUid + " of " + email + " was imported");
106
-
107
-			}
108
-		} catch (IOException e) {
109
-			throw new CliException("ERROR importing calendar for : " + email, e);
110
-		}
111
-	}
91
+		if (!dry) {
92
+			ctx.adminApi().instance(IVEvent.class, calendarUid).importIcs(cliUtils.getStreamFromFile(icsFilePath));
93
+			ctx.info("calendar " + calendarUid + " of " + email + " was imported");
94
+		} else {
95
+			ctx.info("DRY : calendar " + calendarUid + " of " + email + " was imported");
112 96
 
113
-	private Stream getIcsFromFile(String filename) throws IOException {
114
-		try (InputStream in = Files.newInputStream(Paths.get(filename))) {
115
-			GenericStream<?> stream = new GenericStream<byte[]>() {
116
-
117
-				@Override
118
-				protected Buffer serialize(byte[] data) throws Exception {
119
-					return new Buffer(data);
120
-				}
121
-
122
-				@Override
123
-				protected StreamState<byte[]> next() throws Exception {
124
-					byte[] buffer = new byte[1024];
125
-					int count = in.read(buffer);
126
-					if (count == -1) {
127
-						return StreamState.end();
128
-					} else if (count != buffer.length) {
129
-						byte[] data = new byte[count];
130
-						System.arraycopy(buffer, 0, data, 0, count);
131
-						return StreamState.data(data);
132
-					} else {
133
-						return StreamState.data(buffer);
134
-					}
135
-				}
136
-
137
-			};
138
-			return VertxStream.stream(stream);
139 97
 		}
140 98
 
141 99
 	}
... ...
@@ -17,10 +17,15 @@ Require-Bundle: org.eclipse.core.runtime,
17 17
  net.bluemind.authentication.mgmt.api,
18 18
  com.google.guava,
19 19
  net.bluemind.core.commons,
20
- org.apache.commons.compress;bundle-version="1.6.0",
20
+ org.apache.commons.compress,
21 21
  net.bluemind.cli.calendar,
22 22
  net.bluemind.cli.contact,
23
- net.bluemind.cli.todolist
23
+ net.bluemind.cli.todolist,
24
+ net.bluemind.calendar.api,
25
+ net.bluemind.todolist.api,
26
+ net.bluemind.server.api,
27
+ net.bluemind.config,
28
+ net.bluemind.utils
24 29
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
25 30
 Automatic-Module-Name: net.bluemind.cli.user
26 31
 Bundle-ActivationPolicy: lazy
... ...
@@ -19,10 +19,14 @@
19 19
             impl="net.bluemind.cli.user.UserLogoutCommand$Reg"
20 20
             priority="90">
21 21
       </registration>
22
-            <registration
22
+      <registration
23 23
             impl="net.bluemind.cli.user.UserExportCommand$Reg"
24 24
             priority="80">
25 25
       </registration>
26
+      <registration
27
+            impl="net.bluemind.cli.user.UserImportCommand$Reg"
28
+            priority="79">
29
+      </registration>
26 30
    </extension>
27 31
 
28 32
 </plugin>
... ...
@@ -45,6 +45,7 @@ import net.bluemind.core.container.model.ItemValue;
45 45
 import net.bluemind.directory.api.BaseDirEntry.Kind;
46 46
 import net.bluemind.directory.api.DirEntry;
47 47
 import net.bluemind.user.api.IUser;
48
+import net.bluemind.utils.FileUtils;
48 49
 
49 50
 @Command(name = "export", description = "export user data to an archive file")
50 51
 public class UserExportCommand extends SingleOrDomainOperation {
... ...
@@ -64,23 +65,23 @@ public class UserExportCommand extends SingleOrDomainOperation {
64 65
 
65 66
 	public String outputDir = "/tmp/bm-export";
66 67
 
67
-	public UserExportCommand() {
68
-	}
69
-
70 68
 	@Override
71 69
 	public void synchronousDirOperation(String domainUid, ItemValue<DirEntry> de) {
72 70
 		outputDir = outputDir + "/" + UUID.randomUUID();
73 71
 		File dir = new File(outputDir);
74
-		dir.mkdirs();
75
-		dir.deleteOnExit();
72
+		try {
73
+			dir.mkdirs();
74
+			Arrays.asList("contact", "calendar", "task").forEach(data -> exportData(de, data));
76 75
 
77
-		Arrays.asList("contact", "calendar", "task").forEach(data -> exportData(de, data));
76
+			createEmailSymlink(domainUid, de);
78 77
 
79
-		createEmailSymlink(domainUid, de);
78
+			ctx.info("Creating archive file, can take a moment...");
79
+			File archiveFile = createArchive(de);
80
+			ctx.info("Archive file for " + de.value.email + " created as : " + archiveFile.getAbsolutePath());
81
+		} finally {
82
+			FileUtils.delete(dir);
83
+		}
80 84
 
81
-		ctx.info("Creating archive file, can take a moment...");
82
-		File archiveFile = createArchive(de);
83
-		ctx.info("Archive file for " + de.value.email + " created as : " + archiveFile.getAbsolutePath());
84 85
 	}
85 86
 
86 87
 	private File createArchive(ItemValue<DirEntry> de) {
87 88
new file mode 100644
... ...
@@ -0,0 +1,313 @@
1
+/* BEGIN LICENSE
2
+  * Copyright © Blue Mind SAS, 2012-2018
3
+  *
4
+  * This file is part of BlueMind. BlueMind is a messaging and collaborative
5
+  * solution.
6
+  *
7
+  * This program is free software; you can redistribute it and/or modify
8
+  * it under the terms of either the GNU Affero General Public License as
9
+  * published by the Free Software Foundation (version 3 of the License).
10
+  *
11
+  * This program is distributed in the hope that it will be useful,
12
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14
+  *
15
+  * See LICENSE.txt
16
+  * END LICENSE
17
+  */
18
+package net.bluemind.cli.user;
19
+
20
+import java.io.BufferedOutputStream;
21
+import java.io.File;
22
+import java.io.FileOutputStream;
23
+import java.io.IOException;
24
+import java.io.InputStream;
25
+import java.nio.file.Files;
26
+import java.nio.file.Path;
27
+import java.nio.file.Paths;
28
+import java.util.Optional;
29
+import java.util.UUID;
30
+
31
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
32
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
33
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
34
+
35
+import io.airlift.airline.Command;
36
+import io.airlift.airline.Option;
37
+import net.bluemind.addressbook.api.AddressBookDescriptor;
38
+import net.bluemind.addressbook.api.IAddressBookUids;
39
+import net.bluemind.addressbook.api.IAddressBooksMgmt;
40
+import net.bluemind.addressbook.api.IVCardService;
41
+import net.bluemind.calendar.api.CalendarContainerType;
42
+import net.bluemind.calendar.api.CalendarDescriptor;
43
+import net.bluemind.calendar.api.ICalendarsMgmt;
44
+import net.bluemind.calendar.api.IVEvent;
45
+import net.bluemind.cli.cmd.api.CliException;
46
+import net.bluemind.cli.cmd.api.ICmdLet;
47
+import net.bluemind.cli.cmd.api.ICmdLetRegistration;
48
+import net.bluemind.cli.directory.common.SingleOrDomainOperation;
49
+import net.bluemind.cli.utils.Tasks;
50
+import net.bluemind.config.InstallationId;
51
+import net.bluemind.core.container.model.ContainerDescriptor;
52
+import net.bluemind.core.container.model.ItemValue;
53
+import net.bluemind.core.task.api.TaskRef;
54
+import net.bluemind.directory.api.BaseDirEntry.Kind;
55
+import net.bluemind.directory.api.DirEntry;
56
+import net.bluemind.server.api.IServer;
57
+import net.bluemind.todolist.api.ITodoLists;
58
+import net.bluemind.todolist.api.ITodoUids;
59
+import net.bluemind.todolist.api.IVTodo;
60
+import net.bluemind.user.api.IUser;
61
+import net.bluemind.utils.FileUtils;
62
+
63
+@Command(name = "import", description = "import user data from an archive file, existing data will be erased.")
64
+public class UserImportCommand extends SingleOrDomainOperation {
65
+
66
+	public static class Reg implements ICmdLetRegistration {
67
+
68
+		@Override
69
+		public Optional<String> group() {
70
+			return Optional.of("user");
71
+		}
72
+
73
+		@Override
74
+		public Class<? extends ICmdLet> commandClass() {
75
+			return UserImportCommand.class;
76
+		}
77
+	}
78
+
79
+	private static final int BUFFER_MAX_SIZE = 1024;
80
+
81
+	@Option(name = "--archiveFile", required = true, description = "BM user archive path")
82
+	public String archiveFile = null;
83
+
84
+	private String domainUid;
85
+
86
+	@Override
87
+	public void synchronousDirOperation(String domainUid, ItemValue<DirEntry> de) throws IOException {
88
+		this.domainUid = domainUid;
89
+		File archive = new File(archiveFile);
90
+		if (!archive.exists() || archive.isDirectory()) {
91
+			throw new CliException("Invalid archive file");
92
+		}
93
+
94
+		Path tempDir = Files.createDirectory(Paths.get("bm-import"));
95
+		try {
96
+			extractArchive(archive.toPath(), tempDir);
97
+
98
+			// skip first directory level
99
+			tempDir = Files.list(tempDir).findFirst().get();
100
+			importDatas(de, tempDir);
101
+
102
+			IServer serversApi = ctx.adminApi().instance(IServer.class, InstallationId.getIdentifier());
103
+
104
+			serversApi.submitAndWait(de.value.dataLocation,
105
+					"bm-cli maintenance repair --ops mailboxFilesystem " + de.value.email);
106
+			serversApi.submitAndWait(de.value.dataLocation,
107
+					"bm-cli maintenance repair --ops mailboxAcls " + de.value.email);
108
+		} catch (IOException e) {
109
+			ctx.error("Error extracting archive " + e.getMessage());
110
+			throw new CliException(e);
111
+		} finally {
112
+			FileUtils.delete(tempDir.toFile());
113
+		}
114
+	}
115
+
116
+	private void extractArchive(Path archivePath, Path tempDir) throws IOException {
117
+
118
+		InputStream in = Files.newInputStream(archivePath);
119
+
120
+		GzipCompressorInputStream gzipIn = new GzipCompressorInputStream(in);
121
+		try (TarArchiveInputStream tarIn = new TarArchiveInputStream(gzipIn)) {
122
+			TarArchiveEntry entry;
123
+
124
+			while ((entry = (TarArchiveEntry) tarIn.getNextEntry()) != null) {
125
+				Path path = Paths.get(tempDir.toString(), entry.getName());
126
+
127
+				/** If the entry is a directory, create the directory. **/
128
+				if (entry.isDirectory()) {
129
+					Files.createDirectories(path);
130
+				} else {
131
+					int size = (int) entry.getSize();
132
+					if (size == 0) {
133
+						continue;
134
+					}
135
+					byte data[] = new byte[size];
136
+
137
+					try (FileOutputStream fos = new FileOutputStream(path.toString(), false);
138
+							BufferedOutputStream dest = new BufferedOutputStream(fos, size)) {
139
+						int count = 0;
140
+						while ((count = tarIn.read(data, 0, BUFFER_MAX_SIZE)) != -1) {
141
+							dest.write(data, 0, count);
142
+						}
143
+					}
144
+				}
145
+			}
146
+			ctx.info("Archive file extracted successfully to " + tempDir.toString());
147
+		}
148
+
149
+	}
150
+
151
+	private void importDatas(ItemValue<DirEntry> de, Path tempDir) throws IOException {
152
+		Files.list(tempDir).forEach(s -> {
153
+			try {
154
+				Files.list(s).forEach(subData -> {
155
+					try {
156
+						switch (s.getFileName().toString()) {
157
+						case "contact":
158
+							importContacts(de, subData);
159
+							break;
160
+						case "calendar":
161
+							importCalendars(de, subData);
162
+							break;
163
+						case "task":
164
+							importTasks(de, subData);
165
+							break;
166
+						case "mail":
167
+							importMail(de, subData);
168
+							break;
169
+						default:
170
+							ctx.error("Unknown data directory : " + s);
171
+							break;
172
+						}
173
+					} catch (IOException e) {
174
+						throw new CliException(e);
175
+					}
176
+				});
177
+			} catch (Exception e) {
178
+				throw new CliException("Error importing data", e);
179
+			}
180
+		});
181
+	}
182
+
183
+	private void importCalendars(ItemValue<DirEntry> de, Path dir) throws IOException {
184
+		Files.list(dir).forEach(cal -> importCalendar(de, cal));
185
+	}
186
+
187
+	private void importCalendar(ItemValue<DirEntry> de, Path icsFile) {
188
+		String calName = decodeName(icsFile);
189
+		ctx.info("Importing calendar : " + calName + " path : " + icsFile.toString());
190
+
191
+		// find calendar
192
+		String calUid = null;
193
+		if (calName.equals(de.displayName)) {
194
+			calUid = CalendarContainerType.defaultUserCalendar(de.uid);
195
+		} else {
196
+			// create a new one
197
+			ICalendarsMgmt calMgmt = ctx.adminApi().instance(ICalendarsMgmt.class);
198
+			CalendarDescriptor desc = new CalendarDescriptor();
199
+			desc.domainUid = domainUid;
200
+			desc.name = calName;
201
+			desc.owner = de.uid;
202
+			calUid = UUID.randomUUID().toString();
203
+			calMgmt.create(calUid, desc);
204
+		}
205
+
206
+		TaskRef ref = ctx.adminApi().instance(IVEvent.class, calUid)
207
+				.importIcs(cliUtils.getStreamFromFile(icsFile.toString()));
208
+		Tasks.follow(ctx, false, ref);
209
+	}
210
+
211
+	private void importContacts(ItemValue<DirEntry> de, Path dir) throws IOException {
212
+		Files.list(dir).forEach(cal -> importContact(de, cal));
213
+	}
214
+
215
+	private void importContact(ItemValue<DirEntry> de, Path vcfFile) {
216
+		String abName = decodeName(vcfFile);
217
+		ctx.info("Importing addressbook : " + abName);
218
+
219
+		// find ab
220
+		String abUid = null;
221
+		if (abName.equals("Mes contacts")) {
222
+			abUid = IAddressBookUids.defaultUserAddressbook(de.uid);
223
+		} else if (abName.equals("Contacts collectés")) {
224
+			abUid = IAddressBookUids.collectedContactsUserAddressbook(de.uid);
225
+		} else {
226
+			// create a new one
227
+			IAddressBooksMgmt abMgmt = ctx.adminApi().instance(IAddressBooksMgmt.class, domainUid);
228
+			AddressBookDescriptor desc = new AddressBookDescriptor();
229
+			desc.domainUid = domainUid;
230
+			desc.name = abName;
231
+			desc.owner = de.uid;
232
+			abUid = UUID.randomUUID().toString();
233
+			abMgmt.create(abUid, desc, false);
234
+		}
235
+
236
+		try {
237
+			TaskRef ref = ctx.adminApi().instance(IVCardService.class, abUid)
238
+					.importCards(new String(Files.readAllBytes(vcfFile)));
239
+			Tasks.follow(ctx, false, ref);
240
+		} catch (Exception e) {
241
+			throw new CliException("Error importing addressbook " + vcfFile.toString(), e);
242
+		}
243
+	}
244
+
245
+	private void importTasks(ItemValue<DirEntry> de, Path dir) throws IOException {
246
+		Files.list(dir).forEach(cal -> importTask(de, cal));
247
+	}
248
+
249
+	private void importTask(ItemValue<DirEntry> de, Path vcfFile) {
250
+		String name = decodeName(vcfFile);
251
+		ctx.info("Importing todolist : " + name);
252
+
253
+		// find ab
254
+		String uid = null;
255
+		if (name.equals("Mes tâches")) {
256
+			uid = ITodoUids.defaultUserTodoList(de.uid);
257
+		} else {
258
+			// create a new one
259
+			ITodoLists mgmt = ctx.adminApi().instance(ITodoLists.class, domainUid);
260
+			ContainerDescriptor desc = new ContainerDescriptor();
261
+			desc.domainUid = domainUid;
262
+			desc.name = name;
263
+			desc.owner = de.uid;
264
+			uid = UUID.randomUUID().toString();
265
+			mgmt.create(uid, desc);
266
+		}
267
+
268
+		try {
269
+			TaskRef ref = ctx.adminApi().instance(IVTodo.class, uid).importIcs(new String(Files.readAllBytes(vcfFile)));
270
+			Tasks.follow(ctx, false, ref);
271
+		} catch (Exception e) {
272
+			throw new CliException("Error importing todolist " + vcfFile.toString(), e);
273
+		}
274
+	}
275
+
276
+	private void importMail(ItemValue<DirEntry> de, Path directory) {
277
+		String type = directory.getFileName().toString();
278
+
279
+		ctx.info("Importing mail " + type);
280
+		String login = ctx.adminApi().instance(IUser.class, domainUid).getComplete(de.uid).value.login;
281
+
282
+		String cyrusPath = "/var/spool/cyrus/" + type + "/" + de.value.dataLocation + "__" + domainUid.replace('.', '_')
283
+				+ "/domain/" + domainUid.charAt(0) + "/" + domainUid + "/" + firstLetterMailbox(login) + "/user/"
284
+				+ login.replace('.', '^');
285
+
286
+		copyEmails(de, directory, cyrusPath);
287
+	}
288
+
289
+	private void copyEmails(ItemValue<DirEntry> de, Path directory, String outputDir) {
290
+		String command = String.format("rsync -r %s/ %s", directory.toAbsolutePath().toString(), outputDir);
291
+		IServer serversApi = ctx.adminApi().instance(IServer.class, InstallationId.getIdentifier());
292
+		serversApi.submitAndWait(de.value.dataLocation, command);
293
+	}
294
+
295
+	private char firstLetterMailbox(String mbox) {
296
+		Character c = mbox.charAt(0);
297
+		if (Character.isDigit(c)) {
298
+			return 'q';
299
+		} else {
300
+			return c.charValue();
301
+		}
302
+	}
303
+
304
+	private String decodeName(Path file) {
305
+		return cliUtils
306
+				.decodeFilename(file.getFileName().toString().substring(0, file.getFileName().toString().length() - 4));
307
+	}
308
+
309
+	@Override
310
+	public Kind[] getDirEntryKind() {
311
+		return new Kind[] { Kind.USER };
312
+	}
313
+}
0 314
\ No newline at end of file
... ...
@@ -9,6 +9,7 @@ Require-Bundle: org.eclipse.core.runtime,
9 9
  net.bluemind.core.task.api,
10 10
  net.bluemind.cli.cmd.api,
11 11
  com.google.guava,
12
+ slf4j.api,
12 13
  net.bluemind.domain.api,
13 14
  net.bluemind.directory.api,
14 15
  net.bluemind.core.container.api
... ...
@@ -1,21 +1,30 @@
1 1
 package net.bluemind.cli.utils;
2 2
 
3
+import java.io.IOException;
4
+import java.io.InputStream;
3 5
 import java.io.UnsupportedEncodingException;
6
+import java.nio.file.Files;
7
+import java.nio.file.Paths;
4 8
 import java.util.Arrays;
5 9
 
10
+import org.vertx.java.core.buffer.Buffer;
11
+
6 12
 import net.bluemind.cli.cmd.api.CliContext;
7 13
 import net.bluemind.cli.cmd.api.CliException;
8 14
 import net.bluemind.core.api.ListResult;
15
+import net.bluemind.core.api.Stream;
9 16
 import net.bluemind.core.container.model.ItemValue;
17
+import net.bluemind.core.rest.base.GenericStream;
18
+import net.bluemind.core.rest.vertx.VertxStream;
19
+import net.bluemind.directory.api.BaseDirEntry.Kind;
10 20
 import net.bluemind.directory.api.DirEntry;
11 21
 import net.bluemind.directory.api.DirEntryQuery;
12 22
 import net.bluemind.directory.api.IDirectory;
13
-import net.bluemind.directory.api.BaseDirEntry.Kind;
14 23
 import net.bluemind.domain.api.Domain;
15 24
 import net.bluemind.domain.api.IDomains;
16 25
 
17 26
 public class CliUtils {
18
-	
27
+
19 28
 	CliContext cliContext;
20 29
 
21 30
 	public CliUtils(CliContext cliContext) {
... ...
@@ -29,11 +38,11 @@ public class CliUtils {
29 38
 			return getDomainUidFromDomain(s);
30 39
 		}
31 40
 	}
32
-	
41
+
33 42
 	public String getDomainUidFromEmail(String email) {
34 43
 		return getDomainUidFromDomain(email.split("@")[1]);
35 44
 	}
36
-	
45
+
37 46
 	public String getDomainUidFromDomain(String domainString) {
38 47
 		if ("global.virt".equals(domainString)) {
39 48
 			return "global.virt";
... ...
@@ -45,7 +54,7 @@ public class CliUtils {
45 54
 		}
46 55
 		return domain.uid;
47 56
 	}
48
-	
57
+
49 58
 	public String getUserUidFromEmail(String email) {
50 59
 		String domainUid = getDomainUidFromEmail(email);
51 60
 		IDirectory dirApi = cliContext.adminApi().instance(IDirectory.class, domainUid);
... ...
@@ -58,12 +67,59 @@ public class CliUtils {
58 67
 		}
59 68
 		return result.values.get(0).uid;
60 69
 	}
61
-	
70
+
62 71
 	public String encodeFilename(String name) {
63 72
 		try {
64 73
 			return java.net.URLEncoder.encode(name, "UTF-8");
65 74
 		} catch (UnsupportedEncodingException e) {
66
-			throw new CliException("Encoding error : " + e.getMessage());	
75
+			throw new CliException("Encoding error : " + e.getMessage());
76
+		}
77
+	}
78
+
79
+	public String decodeFilename(String name) {
80
+		try {
81
+			return java.net.URLDecoder.decode(name, "UTF-8");
82
+		} catch (UnsupportedEncodingException e) {
83
+			throw new CliException("Decoding error : " + e.getMessage());
84
+		}
85
+	}
86
+
87
+	public Stream getStreamFromFile(String filename) {
88
+		InputStream in;
89
+		try {
90
+			in = Files.newInputStream(Paths.get(filename));
91
+		} catch (IOException e) {
92
+			throw new CliException(e);
67 93
 		}
94
+		GenericStream<?> stream = new GenericStream<byte[]>() {
95
+
96
+			@Override
97
+			protected Buffer serialize(byte[] data) throws Exception {
98
+				return new Buffer(data);
99
+			}
100
+
101
+			@Override
102
+			protected StreamState<byte[]> next() throws Exception {
103
+				byte[] buffer = new byte[1024];
104
+				int count = in.read(buffer);
105
+				if (count == -1) {
106
+					return StreamState.end();
107
+				} else if (count != buffer.length) {
108
+					byte[] data = new byte[count];
109
+					System.arraycopy(buffer, 0, data, 0, count);
110
+					return StreamState.data(data);
111
+				} else {
112
+					return StreamState.data(buffer);
113
+				}
114
+			}
115
+
116
+		};
117
+		stream.endHandler(v -> {
118
+			try {
119
+				in.close();
120
+			} catch (IOException e) {
121
+			}
122
+		});
123
+		return VertxStream.stream(stream);
68 124
 	}
69 125
 }
... ...
@@ -29,16 +29,22 @@ import net.bluemind.core.task.api.TaskStatus;
29 29
 public class Tasks {
30 30
 
31 31
 	public static TaskStatus follow(CliContext ctx, TaskRef ref) {
32
+		return follow(ctx, true, ref);
33
+	}
34
+
35
+	public static TaskStatus follow(CliContext ctx, boolean shouldLog, TaskRef ref) {
32 36
 		ITask trackApi = ctx.adminApi().instance(ITask.class, ref.id);
33 37
 		TaskStatus ts = null;
34 38
 		int logIdx = 0;
35 39
 		do {
36 40
 			ts = trackApi.status();
37
-			List<String> logs = trackApi.getCurrentLogs();
38
-			for (; logIdx < logs.size(); logIdx++) {
39
-				String log = logs.get(logIdx);
40
-				if (!Strings.isNullOrEmpty(log)) {
41
-					ctx.info(log);
41
+			if (shouldLog) {
42
+				List<String> logs = trackApi.getCurrentLogs();
43
+				for (; logIdx < logs.size(); logIdx++) {
44
+					String log = logs.get(logIdx);
45
+					if (!Strings.isNullOrEmpty(log)) {
46
+						ctx.info(log);
47
+					}
42 48
 				}
43 49
 			}
44 50
 			if (!ts.state.ended) {