Skip to content

Commit b277993

Browse files
brendanx67claude
andcommitted
Added documentation support to Skyline Tool Store
* Extract docs from tool-inf/docs/ in uploaded ZIPs and serve via WebDAV * Carry forward docs from previous version when new ZIP has none * Show "Online Documentation" link in Documentation box when docs exist * Skip tool-inf/docs/ images during icon extraction * Exclude docs dir from supplementary file listings * Fall back to /home/support when no tool-specific support board exists Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 389a437 commit b277993

3 files changed

Lines changed: 79 additions & 5 deletions

File tree

SkylineToolsStore/src/org/labkey/skylinetoolsstore/SkylineToolsStoreController.java

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ protected SkylineTool getToolFromZip(MultipartFile zip) throws IOException
178178
while ((zipEntry = zipStream.getNextEntry()) != null &&
179179
(tool == null || tool.getIcon() == null))
180180
{
181-
if (zipEntry.getName().toLowerCase().startsWith("tool-inf/"))
181+
String entryLower = zipEntry.getName().toLowerCase();
182+
if (entryLower.startsWith("tool-inf/") && !entryLower.startsWith("tool-inf/docs/"))
182183
{
183184
String lowerBaseName = new File(zipEntry.getName()).getName().toLowerCase();
184185

@@ -229,6 +230,39 @@ protected byte[] unzip(ZipInputStream stream)
229230
}
230231
}
231232

233+
protected static boolean extractDocsFromZip(File zipFile, File containerDir) throws IOException
234+
{
235+
File docsDir = new File(containerDir, "docs");
236+
boolean extracted = false;
237+
try (ZipFile zf = new ZipFile(zipFile))
238+
{
239+
Enumeration<? extends ZipEntry> entries = zf.entries();
240+
while (entries.hasMoreElements())
241+
{
242+
ZipEntry entry = entries.nextElement();
243+
String name = entry.getName();
244+
if (!name.toLowerCase().startsWith("tool-inf/docs/") || entry.isDirectory())
245+
continue;
246+
// Strip "tool-inf/docs/" prefix to get relative path within docs dir
247+
String relativePath = name.substring("tool-inf/docs/".length());
248+
if (relativePath.isEmpty())
249+
continue;
250+
File destFile = new File(docsDir, relativePath);
251+
// Zip-slip protection
252+
if (!destFile.getCanonicalPath().startsWith(docsDir.getCanonicalPath() + File.separator))
253+
throw new IOException("Zip entry outside target directory: " + name);
254+
Files.createDirectories(destFile.getParentFile().toPath());
255+
try (InputStream in = zf.getInputStream(entry);
256+
FileOutputStream out = new FileOutputStream(destFile))
257+
{
258+
in.transferTo(out);
259+
}
260+
extracted = true;
261+
}
262+
}
263+
return extracted;
264+
}
265+
232266
public static File makeFile(Container c, String filename)
233267
{
234268
return new File(getLocalPath(c), FileUtil.makeLegalName(filename));
@@ -408,7 +442,7 @@ public static HashSet<String> getSupplementaryFileBasenames(SkylineTool tool)
408442
for (String suppFile : localToolDir.list())
409443
{
410444
final String basename = new File(suppFile).getName();
411-
if (!basename.startsWith(".") && !basename.equals(tool.getZipName()) && !basename.equals("icon.png"))
445+
if (!basename.startsWith(".") && !basename.equals(tool.getZipName()) && !basename.equals("icon.png") && !basename.equals("docs"))
412446
suppFiles.add(suppFile);
413447
}
414448
return suppFiles;
@@ -583,9 +617,19 @@ else if (!getContainer().hasPermission(getUser(), InsertPermission.class))
583617
{
584618
Container c = makeContainer(getContainer(), folderName, toolOwnersUsers, RoleManager.getRole(EditorRole.class));
585619
copyContainerPermissions(existingVersionContainer, c);
586-
zip.transferTo(makeFile(c, zip.getOriginalFilename()));
620+
File storedZip = makeFile(c, zip.getOriginalFilename());
621+
zip.transferTo(storedZip);
587622
tool.writeIconToFile(makeFile(c, "icon.png"), "png");
588623

624+
// Extract docs from tool-inf/docs/ in the ZIP; carry forward from previous version if absent
625+
boolean hasDocs = extractDocsFromZip(storedZip, getLocalPath(c));
626+
if (!hasDocs && existingVersionContainer != null)
627+
{
628+
File oldDocs = new File(getLocalPath(existingVersionContainer), "docs");
629+
if (oldDocs.isDirectory())
630+
FileUtils.copyDirectory(oldDocs, new File(getLocalPath(c), "docs"));
631+
}
632+
589633
if (copyFiles != null && existingVersionContainer != null)
590634
for (String copyFile : copyFiles)
591635
FileUtils.copyFile(makeFile(existingVersionContainer, copyFile), makeFile(c, copyFile), true);

SkylineToolsStore/src/org/labkey/skylinetoolsstore/model/SkylineTool.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,16 @@ public String getFolderUrl()
293293
return AppProps.getInstance().getContextPath() + "/files" + lookupContainer().getPath() + "/";
294294
}
295295

296+
public boolean hasDocumentation()
297+
{
298+
return new File(SkylineToolsStoreController.getLocalPath(lookupContainer()), "docs/index.html").exists();
299+
}
300+
301+
public String getDocsUrl()
302+
{
303+
return AppProps.getInstance().getContextPath() + "/_webdav" + lookupContainer().getPath() + "/@files/docs/index.html";
304+
}
305+
296306
public String getIconUrl()
297307
{
298308
return (SkylineToolsStoreController.makeFile(lookupContainer(), "icon.png").exists()) ?

SkylineToolsStore/src/org/labkey/skylinetoolsstore/view/SkylineToolDetails.jsp

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<%@ page import="org.apache.commons.lang3.StringUtils" %>
2+
<%@ page import="org.labkey.api.data.Container" %>
3+
<%@ page import="org.labkey.api.data.ContainerManager" %>
24
<%@ page import="org.labkey.api.portal.ProjectUrls" %>
35
<%@ page import="org.labkey.api.security.permissions.DeletePermission" %>
46
<%@ page import="org.labkey.api.security.permissions.InsertPermission" %>
@@ -299,7 +301,17 @@ a { text-decoration: none; }
299301
</div>
300302

301303
<button id="tool-support-board-btn" class="banner-button-small">Support Board</button>
302-
<% addHandler("tool-support-board-btn", "click", "window.open(" + q(urlProvider(ProjectUrls.class).getBeginURL(getContainer().getChild("Support").getChild(tool.getName()))) + ", '_blank', 'noopener,noreferrer')"); %>
304+
<%
305+
Container supportContainer = getContainer().getChild("Support");
306+
Container toolSupportBoard = supportContainer != null ? supportContainer.getChild(tool.getName()) : null;
307+
Container supportTarget;
308+
if (toolSupportBoard != null)
309+
supportTarget = toolSupportBoard;
310+
else
311+
supportTarget = ContainerManager.getForPath("/home/support");
312+
if (supportTarget != null)
313+
addHandler("tool-support-board-btn", "click", "window.open(" + q(urlProvider(ProjectUrls.class).getBeginURL(supportTarget)) + ", '_blank', 'noopener,noreferrer')");
314+
%>
303315
</div>
304316
<% if (toolEditor) { %>
305317
<div class="menuMouseArea sprocket">
@@ -332,9 +344,17 @@ a { text-decoration: none; }
332344
</div>
333345
</div>
334346

335-
<% if (suppIter.hasNext()) { %>
347+
<% if (tool.hasDocumentation() || suppIter.hasNext()) { %>
336348
<div id="documentationbox" class="itemsbox">
337349
<legend>Documentation</legend>
350+
<% if (tool.hasDocumentation()) { %>
351+
<div class="barItem">
352+
<a href="<%=h(tool.getDocsUrl())%>" target="_blank" rel="noopener noreferrer">
353+
<img src="<%= h(imgDir) %>link.png" alt="Documentation" />
354+
<span>Online Documentation</span>
355+
</a>
356+
</div>
357+
<% } %>
338358
<%
339359
while (suppIter.hasNext()) {
340360
Map.Entry suppPair = (Map.Entry)suppIter.next();

0 commit comments

Comments
 (0)