TexArchiveService.java
/*
* Copyright © 2022-2025 The CTAN Team and individual authors
*
* This file is distributed under the 3-clause BSD license.
* See file LICENSE for details.
*/
package org.ctan.site.services.texarchive;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.ctan.site.CtanConfiguration;
import org.ctan.site.services.DateUtils;
import org.ctan.site.services.texarchive.PkgService.PkgTo;
import org.ctan.site.services.texarchive.readme.ReadmeReader;
import org.ctan.site.services.util.ConfigUtils;
import org.ctan.site.stores.TexArchiveNotesStore;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Getter;
import lombok.NonNull;
/**
* The class <code>TexarchiveService</code> contains the service to access the
* <span>T<span style=
* "text-transform:uppercase;font-size:90%;vertical-align:-0.4ex;
* margin-left:-0.2em;margin-right:-0.1em;line-height: 0;" >e</span>X</span>
* archive directory.
*
* @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
*/
public class TexArchiveService {
/**
* The class <code>FileTo</code> contains the transport object for the list
* resource.
*/
@Getter
@Builder
@AllArgsConstructor
public static class FileTo {
private String name;
private long size;
private String symlink;
private String mtime;
private char type;
}
/**
* The class <code>ReadmeTo</code> contains the transport object for the
* index resource.
*/
@Getter
@Builder
@AllArgsConstructor
public static class ReadmeTo {
private String name;
private String content;
}
/**
* The class <code>TexarchiveListTo</code> contains a transport object for
* the list resource.
*/
@Getter
@Builder
@AllArgsConstructor
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public static class TexarchiveListTo {
private String text;
private PkgTo pkg;
private ReadmeTo readme;
private String lang;
private int page;
@Default
private int size = 64;
private boolean download;
private List<FileTo> files;
}
/**
* The field <code>base</code> contains directory containing the TeX
* archive.
*/
private String base = null;
/**
* The field <code>texArchiveFileInfoStore</code> contains the info store.
*/
private TexArchiveNotesStore texArchiveNotesStore;
/**
* The field <code>defaultLang</code> contains the default language.
*/
private String defaultLang;
/**
* The field <code>pkgService</code> contains the package service.
*/
private PkgService pkgService;
/**
* This is the constructor for the class <code>TexarchiveService</code>.
*
* @param config the CTAN configuration
* @param pkgService the package service
* @param texArchiveFileInfoStore the store for infos
*/
@SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
public TexArchiveService(CtanConfiguration config,
@NonNull PkgService pkgService,
@NonNull TexArchiveNotesStore texArchiveFileInfoStore) {
this.pkgService = pkgService;
this.defaultLang = ConfigUtils.defaultLanguage(config);
this.texArchiveNotesStore = texArchiveFileInfoStore;
base = config.getTexArchive().getDirectory();
if (base == null) {
throw new IllegalArgumentException(
"Missing configuration texarchive.directory");
}
if (!new File(base).isDirectory()) {
throw new IllegalArgumentException(
"texarchive.directory isn't a directory");
}
if (!base.endsWith("/")) {
base = base + "/";
}
}
// /**
// * The method <code>getIndex</code> provides means to retrieve an index
// file
// * for a given directory in the TeX archive.
// *
// * @param path the path in the TeX archive
// * @return the data for the index file
// */
// public ReadmeTo getIndex(String path) {
//
// if (path == null
// || "..".equals(path)
// || path.contains("/../")
// || path.startsWith("../")
// || path.endsWith("/..")) {
// throw new IllegalArgumentException();
// }
// var dir = new File(base + path);
// if (!dir.isDirectory()) {
// return null;
// }
//
// return findIndex(dir);
// }
/**
* The method <code>getList</code> provides means to retrieve the listing
* for one directory.
*
* @param path the path in the <span>T<span style=
* "text-transform:uppercase;font-size:90%;vertical-align:-0.4ex;
* margin-left:-0.2em;margin-right:-0.1em;line-height: 0;" >e</span>
* X</span> archive
* @param lang the language for the notes
* @param page the number of the page starting with 0
* @param size the page size
* @return a list of files and directories
*/
public TexarchiveListTo getList(String path, String lang, int page,
int size) {
if (path == null
|| "..".equals(path)
|| path.contains("/../")
|| path.startsWith("../")
|| path.endsWith("/..")) {
throw new IllegalArgumentException();
}
if (size < 1) {
size = 64;
}
var dir = new File(base + path);
if (!dir.isDirectory()) {
return null;
}
if (lang == null) {
lang = defaultLang;
}
var list = dir
.listFiles(
(file, s) -> !s.endsWith(".zip") && !s.startsWith("."));
if (list == null) {
return null;
}
// Arrays.sort(list,
// (a, b) -> a.getName().compareToIgnoreCase(b.getName()));
List<FileTo> files = Arrays.asList(list).stream()
.sorted((a, b) -> a.getName().compareToIgnoreCase(b.getName()))
// .skip(page * size)
// .limit(size)
.map((File x) -> FileTo.builder()
.name(x.getName())
.size(x.length())
.type(type(x))
.symlink(symlink(x))
.mtime(DateUtils.formatDateTime(x.lastModified()))
.build())
.collect(Collectors.toList());
var pkgTo = ("".equals(path)
? null
: pkgService.getPkgByPath(path, lang));
var info = texArchiveNotesStore.getByPath("/" + path);
ReadmeTo readme = null;
readme = ReadmeReader.findIndex(dir);
String text;
if (pkgTo != null) {
text = pkgTo.getCaption();
} else if (info == null) {
text = "";
} else {
text = switch (lang) {
case "en" -> info.getNoteEn();
case "de" -> info.getNoteDe();
default -> "";
};
}
return TexarchiveListTo.builder()
.download(path.length() > 1
&& new File(base + path + ".zip").isFile())
.files(files)
.lang(lang)
.page(page)
.pkg(pkgTo)
.readme(readme)
.size(size)
.text(text)
.build();
}
/**
* The method <code>symlink</code> provides means to extract the target of a
* symbolic link.
*
* @param file the file to analyse
* @return the path the symlink is pointing to or the empty string
*/
private String symlink(File file) {
try {
var path = file.toPath();
if (Files.isSymbolicLink(path)) {
return Files.readSymbolicLink(path).toString();
}
} catch (IOException e) {
// will not happen
}
return "";
}
/**
* The method <code>type</code> provides means to retrieve the file type as
* a single character.
*
* @param file the file
* @return <code>l</code> if the file is a symlink<br>
* <code>d</code> if the file is a directory<br>
* <code>f</code> else
*/
private char type(File file) {
if (Files.isSymbolicLink(file.toPath())) {
return 'l';
}
return file.isDirectory() ? 'd' : 'f';
}
}