Skip to content

Commit 9b3934d

Browse files
committed
Support directory listing when given a directory URL
1 parent 5dd4433 commit 9b3934d

File tree

7 files changed

+209
-8
lines changed

7 files changed

+209
-8
lines changed

src/main/java/org/commonwl/view/cwl/CWLService.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.commonwl.view.graphviz.ModelDotWriter;
4040
import org.commonwl.view.graphviz.RDFDotWriter;
4141
import org.commonwl.view.workflow.Workflow;
42+
import org.commonwl.view.workflow.WorkflowOverview;
4243
import org.slf4j.Logger;
4344
import org.slf4j.LoggerFactory;
4445
import org.springframework.beans.factory.annotation.Autowired;
@@ -428,6 +429,55 @@ public Workflow parseWorkflowWithCwltool(Workflow basicModel,
428429

429430
}
430431

432+
/**
433+
* Get an overview of a workflow
434+
* @param file A file, potentially a workflow
435+
* @return A constructed WorkflowOverview of the workflow
436+
* @throws IOException Any API errors which may have occurred
437+
*/
438+
public WorkflowOverview getWorkflowOverview(File file) throws IOException {
439+
440+
// Get the content of this file from Github
441+
long fileSizeBytes = file.length();
442+
443+
// Check file size limit before parsing
444+
if (fileSizeBytes <= singleFileSizeLimit) {
445+
446+
// Parse file as yaml
447+
JsonNode cwlFile = yamlStringToJson(readFileToString(file));
448+
449+
// If the CWL file is packed there can be multiple workflows in a file
450+
if (cwlFile.has(DOC_GRAPH)) {
451+
// Packed CWL, find the first subelement which is a workflow and take it
452+
for (JsonNode jsonNode : cwlFile.get(DOC_GRAPH)) {
453+
if (extractProcess(jsonNode) == CWLProcess.WORKFLOW) {
454+
cwlFile = jsonNode;
455+
}
456+
}
457+
}
458+
459+
// Can only make an overview if this is a workflow
460+
if (extractProcess(cwlFile) == CWLProcess.WORKFLOW) {
461+
// Use filename for label if there is no defined one
462+
String label = extractLabel(cwlFile);
463+
if (label == null) {
464+
label = file.getName();
465+
}
466+
467+
// Return the constructed overview
468+
return new WorkflowOverview(file.getName(), label, extractDoc(cwlFile));
469+
} else {
470+
// Return null if not a workflow file
471+
return null;
472+
}
473+
} else {
474+
throw new IOException("File '" + file.getName() + "' is over singleFileSizeLimit - " +
475+
FileUtils.byteCountToDisplaySize(fileSizeBytes) + "/" +
476+
FileUtils.byteCountToDisplaySize(singleFileSizeLimit));
477+
}
478+
479+
}
480+
431481
/**
432482
* Set the format for an input or output, handling ontologies
433483
* @param inputOutput The input or output CWL Element

src/main/java/org/commonwl/view/git/GitDetails.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,13 @@ public String getUrl() {
139139
* @return The URL
140140
*/
141141
public String getInternalUrl() {
142+
String pathPart = path.equals("/") ? "" : "/" + path;
142143
switch (getType()) {
143144
case GITHUB:
144145
case GITLAB:
145-
return "/workflows/" + normaliseUrl(repoUrl).replace(".git", "") + "/blob/" + branch + "/" + path;
146+
return "/workflows/" + normaliseUrl(repoUrl).replace(".git", "") + "/blob/" + branch + pathPart;
146147
default:
147-
return "/workflows/" + normaliseUrl(repoUrl) + "/" + branch + "/" + path;
148+
return "/workflows/" + normaliseUrl(repoUrl) + "/" + branch + pathPart;
148149
}
149150
}
150151

src/main/java/org/commonwl/view/workflow/WorkflowController.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.commonwl.view.git.GitDetails;
2525
import org.commonwl.view.graphviz.GraphVizService;
2626
import org.eclipse.jgit.api.errors.GitAPIException;
27+
import org.eclipse.jgit.api.errors.TransportException;
2728
import org.slf4j.Logger;
2829
import org.slf4j.LoggerFactory;
2930
import org.springframework.beans.factory.annotation.Autowired;
@@ -45,6 +46,7 @@
4546
import javax.validation.Valid;
4647
import java.io.File;
4748
import java.io.IOException;
49+
import java.util.List;
4850

4951
@Controller
5052
public class WorkflowController {
@@ -119,7 +121,14 @@ public ModelAndView createWorkflow(@Valid WorkflowForm workflowForm, BindingResu
119121
Workflow workflow = workflowService.getWorkflow(gitInfo);
120122
if (workflow == null) {
121123
try {
122-
workflow = workflowService.createQueuedWorkflow(gitInfo).getTempRepresentation();
124+
if (gitInfo.getPath().endsWith(".cwl")) {
125+
workflow = workflowService.createQueuedWorkflow(gitInfo).getTempRepresentation();
126+
} else {
127+
return new ModelAndView("redirect:" + gitInfo.getInternalUrl());
128+
}
129+
} catch (TransportException ex) {
130+
bindingResult.rejectValue("url", "git.sshError");
131+
return new ModelAndView("index");
123132
} catch (GitAPIException ex) {
124133
bindingResult.rejectValue("url", "git.retrievalError");
125134
logger.error("Git API Error", ex);
@@ -449,10 +458,24 @@ private ModelAndView getWorkflow(GitDetails gitDetails, RedirectAttributes redir
449458
workflowFormValidator.validateAndParse(workflowForm, errors);
450459
if (!errors.hasErrors()) {
451460
try {
452-
queued = workflowService.createQueuedWorkflow(gitDetails);
461+
if (gitDetails.getPath().endsWith(".cwl")) {
462+
queued = workflowService.createQueuedWorkflow(gitDetails);
463+
} else {
464+
List<WorkflowOverview> workflowOverviews = workflowService.getWorkflowsFromDirectory(gitDetails);
465+
if (workflowOverviews.size() > 1) {
466+
return new ModelAndView("selectworkflow", "workflowOverviews", workflowOverviews)
467+
.addObject("gitDetails", gitDetails);
468+
} else if (workflowOverviews.size() == 1) {
469+
return new ModelAndView("redirect:" + gitDetails.getInternalUrl() +
470+
"/" + workflowOverviews.get(0).getFileName());
471+
} else {
472+
errors.rejectValue("url", "url.noWorkflowsInDirectory", "No workflow files were found in the given directory");
473+
}
474+
}
475+
} catch (TransportException ex) {
476+
errors.rejectValue("url", "git.sshError", "SSH URLs are not supported, please provide a HTTPS URL for the repository or submodules");
453477
} catch (GitAPIException ex) {
454478
errors.rejectValue("url", "git.retrievalError", "The workflow could not be retrieved from the Git repository using the details given");
455-
logger.error("Git API Error", ex);
456479
} catch (WorkflowNotFoundException ex) {
457480
errors.rejectValue("url", "git.pathTraversal", "The path given did not resolve to a location within the repository");
458481
} catch (IOException ex) {

src/main/java/org/commonwl/view/workflow/WorkflowFormValidator.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,19 @@ public class WorkflowFormValidator {
4141
private static final String GITHUB_CWL_REGEX = "^https?:\\/\\/github\\.com\\/([A-Za-z0-9_.-]+)\\/([A-Za-z0-9_.-]+)\\/?(?:tree|blob)\\/([^/]+)(?:\\/(.+\\.cwl))$";
4242
private static final Pattern githubCwlPattern = Pattern.compile(GITHUB_CWL_REGEX);
4343

44+
// URL validation for directories on Github
45+
private static final String GITHUB_DIR_REGEX = "^https?:\\/\\/github\\.com\\/([A-Za-z0-9_.-]+)\\/([A-Za-z0-9_.-]+)\\/?(?:(?:tree|blob)\\/([^/]+)\\/?(.*)?)?$";
46+
private static final Pattern githubDirPattern = Pattern.compile(GITHUB_DIR_REGEX);
47+
4448
// URL validation for cwl files on Gitlab
4549
private static final String GITLAB_CWL_REGEX = "^https?:\\/\\/gitlab\\.com\\/([A-Za-z0-9_.-]+)\\/([A-Za-z0-9_.-]+)\\/?(?:tree|blob)\\/([^/]+)(?:\\/(.+\\.cwl))$";
4650
private static final Pattern gitlabCwlPattern = Pattern.compile(GITLAB_CWL_REGEX);
4751

48-
// Git URL validation
52+
// URL validation for directories on Gitlab
53+
private static final String GITLAB_DIR_REGEX = "^https?:\\/\\/gitlab\\.com\\/([A-Za-z0-9_.-]+)\\/([A-Za-z0-9_.-]+)\\/?(?:(?:tree|blob)\\/([^/]+)\\/?(.*)?)?$";
54+
private static final Pattern gitlabDirPattern = Pattern.compile(GITLAB_DIR_REGEX);
55+
56+
// Generic Git URL validation
4957
private static final String GIT_REPO_REGEX = "^((git|ssh|http(s)?)|(git@[\\w\\.]+))(:(//)?)([\\w\\.@\\:/\\-~]+)(\\.git)(/)?$";
5058
private static final Pattern gitRepoPattern = Pattern.compile(GIT_REPO_REGEX);
5159

@@ -74,9 +82,22 @@ public GitDetails validateAndParse(WorkflowForm form, Errors e) {
7482
return new GitDetails(repoUrl, m.group(3), m.group(4));
7583
}
7684

85+
// Github Dir URL
86+
m = githubDirPattern.matcher(form.getUrl());
87+
if (m.find()) {
88+
String repoUrl = "https://github.com/" + m.group(1) + "/" + m.group(2) + ".git";
89+
return new GitDetails(repoUrl, m.group(3), m.group(4));
90+
}
91+
92+
// Gitlab Dir URL
93+
m = gitlabDirPattern.matcher(form.getUrl());
94+
if (m.find()) {
95+
String repoUrl = "https://gitlab.com/" + m.group(1) + "/" + m.group(2) + ".git";
96+
return new GitDetails(repoUrl, m.group(3), m.group(4));
97+
}
98+
7799
// General Git details if didn't match the above
78100
ValidationUtils.rejectIfEmptyOrWhitespace(e, "branch", "branch.emptyOrWhitespace");
79-
ValidationUtils.rejectIfEmptyOrWhitespace(e, "path", "path.emptyOrWhitespace");
80101
if (!e.hasErrors()) {
81102
m = gitRepoPattern.matcher(form.getUrl());
82103
if (m.find()) {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.commonwl.view.workflow;
21+
22+
/**
23+
* Gives an overview of a workflow
24+
*/
25+
public class WorkflowOverview {
26+
27+
private final String fileName;
28+
private final String label;
29+
private final String doc;
30+
31+
public WorkflowOverview(String fileName, String label, String doc) {
32+
this.fileName = fileName;
33+
this.label = label;
34+
this.doc = doc;
35+
}
36+
37+
public String getFileName() {
38+
return fileName;
39+
}
40+
41+
public String getLabel() {
42+
return label;
43+
}
44+
45+
public String getDoc() {
46+
return doc;
47+
}
48+
49+
}

src/main/java/org/commonwl/view/workflow/WorkflowService.java

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@
4242
import java.io.File;
4343
import java.io.IOException;
4444
import java.nio.file.Path;
45+
import java.util.ArrayList;
4546
import java.util.Calendar;
4647
import java.util.Date;
48+
import java.util.List;
4749

4850
@Service
4951
public class WorkflowService {
@@ -183,6 +185,61 @@ public Workflow getWorkflow(GitDetails gitInfo) {
183185
return workflow;
184186
}
185187

188+
/**
189+
* Get a list of workflows from a directory
190+
* @param gitInfo The Git directory information
191+
* @return The list of workflow overviews
192+
*/
193+
public List<WorkflowOverview> getWorkflowsFromDirectory(GitDetails gitInfo) throws IOException, GitAPIException {
194+
List<WorkflowOverview> workflowsInDir = new ArrayList<>();
195+
try {
196+
Git repo = null;
197+
while (repo == null) {
198+
try {
199+
boolean safeToAccess = gitSemaphore.acquire(gitInfo.getRepoUrl());
200+
repo = gitService.getRepository(gitInfo, safeToAccess);
201+
} catch (RefNotFoundException ex) {
202+
// Attempt slashes in branch fix
203+
GitDetails correctedForSlash = transferPathToBranch(gitInfo);
204+
if (correctedForSlash != null) {
205+
gitSemaphore.release(gitInfo.getRepoUrl());
206+
gitInfo = correctedForSlash;
207+
} else {
208+
throw ex;
209+
}
210+
}
211+
}
212+
213+
Path localPath = repo.getRepository().getWorkTree().toPath();
214+
Path pathToDirectory = localPath.resolve(gitInfo.getPath()).normalize().toAbsolutePath();
215+
if (pathToDirectory.toString().equals("/")) {
216+
pathToDirectory = localPath;
217+
} else if (!pathToDirectory.startsWith(localPath.normalize().toAbsolutePath())) {
218+
// Prevent path traversal attacks
219+
throw new WorkflowNotFoundException();
220+
}
221+
222+
File directory = new File(pathToDirectory.toString());
223+
if (directory.exists() && directory.isDirectory()) {
224+
for (final File file : directory.listFiles()) {
225+
int eIndex = file.getName().lastIndexOf('.') + 1;
226+
if (eIndex > 0) {
227+
String extension = file.getName().substring(eIndex);
228+
if (extension.equals("cwl")) {
229+
WorkflowOverview overview = cwlService.getWorkflowOverview(file);
230+
if (overview != null) {
231+
workflowsInDir.add(overview);
232+
}
233+
}
234+
}
235+
}
236+
}
237+
} finally {
238+
gitSemaphore.release(gitInfo.getRepoUrl());
239+
}
240+
return workflowsInDir;
241+
}
242+
186243
/**
187244
* Get the RO bundle for a Workflow, triggering re-download if it does not exist
188245
* @param gitDetails The origin details of the workflow
@@ -224,7 +281,6 @@ public QueuedWorkflow createQueuedWorkflow(GitDetails gitInfo)
224281
QueuedWorkflow queuedWorkflow;
225282

226283
try {
227-
// Clone repository to temporary folder
228284
Git repo = null;
229285
while (repo == null) {
230286
try {

src/main/resources/messages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ git.retrievalError = The workflow could not be retrieved from the Git repository
66
git.pathTraversal = The path given did not resolve to a location within the repository
77

88
url.parsingError = An unexpected error occurred when parsing the workflow
9+
url.noWorkflowsInDirectory = No workflow files were found in the given directory

0 commit comments

Comments
 (0)