Merge branch 'main' of ssh://git.zander.im/Zander671/curator

This commit is contained in:
Alexander Rosenberg 2024-10-28 15:36:17 -07:00
commit 797615877b
Signed by: Zander671
GPG Key ID: 5FD0394ADBD72730
13 changed files with 288 additions and 128 deletions

View File

@ -12,9 +12,6 @@ public class Start {
public static final String CURATOR_VERSION = "1.0";
public static void main(String[] args) {
// TODO debug
System.setProperty("sun.java2d.uiScale", "2");
registerSecretsFactories();
SwingUtilities.invokeLater(() -> new LibrarySelectFrame(args));
}

View File

@ -117,8 +117,9 @@ public class Library {
private LocalCache localCache;
private RemoteStore remoteStore;
public Library(File local, URI remote, Secrets secrets) throws ManifestParseException, IOException {
public Library(File local, URI remote, boolean passiveMode, Secrets secrets) throws ManifestParseException, IOException {
remoteStore = new RemoteStore(remote, secrets);
remoteStore.setPassiveMode(passiveMode);
localCache = new LocalCache(local);
}

View File

@ -7,6 +7,7 @@ import java.net.ProtocolException;
import java.net.URI;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
@ -34,6 +35,7 @@ public class RemoteStore {
private final FTPClient ftp;
private int connectionLevel = 0;
private boolean ignoreManifest = false;
private boolean passiveMode;
public RemoteStoreFileTypeHandler fileTypeHandler = new RemoteStoreFileTypeHandler();
@ -42,10 +44,14 @@ public class RemoteStore {
if (!url.isAbsolute() || url.getScheme().equals("ftp")) {
ftp = new FTPClient();
} else if (url.getScheme().equals("ftps")) {
ftp = new FTPSClient(false);
ftp = new FTPSClient("TLS", false);
((FTPSClient) ftp).setEndpointCheckingEnabled(true);
} else {
throw new ProtocolException("unknown protocol: '" + url.getScheme() + "'");
}
// Uncomment this to print all FTP commands
// ftp.addProtocolCommandListener(new PrintCommandListener(System.out, true));
ftp.setListHiddenFiles(true);
this.secrets = secrets;
}
@ -61,6 +67,24 @@ public class RemoteStore {
this.ignoreManifest = ignoreManifest;
}
public boolean isPassiveMode() {
return passiveMode;
}
public void setPassiveMode(boolean passiveMode) throws IOException {
this.passiveMode = passiveMode;
if (connectionLevel != 0) {
String modeString = passiveMode ? "passive" : "active";
LOGGER.info("Switching to " + modeString + " mode");
if (passiveMode) {
ftp.enterLocalPassiveMode();
} else {
ftp.enterLocalActiveMode();
}
checkFtpResponse("Failed to switch to " + modeString + " mode");
}
}
public void fetchManifest() throws IOException {
doFtpActions(() -> {
FTPFile info = fileInformation(MANIFEST_PATH);
@ -199,10 +223,27 @@ public class RemoteStore {
return fileInformation(path) != null;
}
private boolean isRootDir(String path) {
return path.matches("^(/+\\.{0,2})+$");
}
public FTPFile fileInformation(String path) throws IOException {
if (isRootDir(path)) {
return null;
}
AtomicReference<FTPFile> file = new AtomicReference<FTPFile>();
doFtpActions(() -> {
file.set(ftp.mlistFile(resolvePath(path)));
String pp = getParentPath(path);
if (pp == null) {
pp = getBasePath();
}
String name = getFileName(path);
FTPFile[] files = ftp.listFiles(pp);
for (FTPFile f : files) {
if (f.getName().equals(name)) {
file.set(f);
}
}
});
return file.get();
}
@ -216,6 +257,14 @@ public class RemoteStore {
throw new IOException("Invalid ftp secrets! Username: '" + secrets.getUsername() + "'");
}
LOGGER.info("Logged info ftp as user '{}'", secrets.getUsername());
if (ftp instanceof FTPSClient) {
FTPSClient ftps = (FTPSClient) ftp;
ftps.execPBSZ(0);
checkFtpResponse("Failed to send PBSZ command");
ftps.execPROT("P");
checkFtpResponse("Failed to send PROT command");
}
setPassiveMode(passiveMode);
}
}
@ -307,7 +356,10 @@ public class RemoteStore {
pb.append("/");
ftp.makeDirectory(pb.toString());
}
checkFtpResponse("Could not create directory: " + pp);
info = fileInformation(pp);
// if (info == null || !info.isDirectory()) {
// throw new IOException("Failed to create directory: " + pp);
// }
LOGGER.info("Created directory: {}", pp);
} else if (!info.isDirectory()) {
throw new IOException("Not a directory: " + pp);
@ -319,8 +371,15 @@ public class RemoteStore {
if (ftp.isConnected()) {
int r = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(r)) {
LOGGER.info(error);
throw new IOException(error);
String replyString = ftp.getReplyString();
if (replyString.endsWith("\n")) {
replyString = replyString.substring(0, replyString.length() - 1);
}
if (replyString.endsWith(".")) {
replyString = replyString.substring(0, replyString.length() - 1);
}
LOGGER.info("{}: {}", error, replyString);
throw new IOException(error + ": " + replyString);
}
}
}
@ -365,6 +424,19 @@ public class RemoteStore {
if (i == -1) {
return null;
}
return cp.substring(0, i);
String pp = cp.substring(0, i);
while (!pp.isEmpty() && pp.charAt(pp.length() - 1) == '/') {
pp = pp.substring(0, pp.length() - 1);
}
return pp;
}
private static String getFileName(String path) {
String cp = cleanPath(path);
int i = cp.lastIndexOf("/");
if (i == -1) {
return cp;
}
return cp.substring(i + 1);
}
}

View File

@ -33,7 +33,7 @@ public class PassSecretsFactory extends SecretsFactory {
public static boolean isBackendSupported() {
try {
Process p = Runtime.getRuntime().exec("which pass");
Process p = Runtime.getRuntime().exec(new String[] {"which", "pass"});
if (!p.waitFor(1000, TimeUnit.MILLISECONDS)) {
return false;
}

View File

@ -12,6 +12,7 @@ import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
@ -44,6 +45,8 @@ public class LibraryCreateDialog extends JDialog {
private final JTextField usernameField;
private final JLabel passwordLabel;
private final JPasswordField passwordField;
private final JLabel ftpModeLabel;
private final JComboBox<String> ftpModeCombo;
private final JCheckBox defaultPathCheck;
private final JLabel pathLabel;
private final FileChooserField pathField;
@ -66,6 +69,8 @@ public class LibraryCreateDialog extends JDialog {
usernameField = new JTextField(10);
passwordLabel = new JLabel("Password:");
passwordField = new JPasswordField(10);
ftpModeLabel = new JLabel("FTP Mode:");
ftpModeCombo = new JComboBox<String>(new String[] { "Passive", "Active" });
defaultPathCheck = new JCheckBox("Use default library location");
defaultPathCheck.setSelected(true);
pathLabel = new JLabel("Library Path:");
@ -223,12 +228,21 @@ public class LibraryCreateDialog extends JDialog {
entryPanel.add(passwordField, gbc);
gbc.gridy = 5;
gbc.gridx = 0;
gbc.fill = GridBagConstraints.NONE;
gbc.anchor = GridBagConstraints.LINE_END;
entryPanel.add(ftpModeLabel, gbc);
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor = GridBagConstraints.LINE_START;
entryPanel.add(ftpModeCombo, gbc);
gbc.gridy = 6;
gbc.gridx = 0;
gbc.anchor = GridBagConstraints.CENTER;
gbc.fill = GridBagConstraints.NONE;
gbc.gridwidth = 2;
entryPanel.add(defaultPathCheck, gbc);
gbc.gridwidth = 1;
gbc.gridy = 6;
gbc.gridy = 7;
gbc.gridx = 0;
gbc.anchor = GridBagConstraints.LINE_END;
entryPanel.add(pathLabel, gbc);
@ -266,4 +280,8 @@ public class LibraryCreateDialog extends JDialog {
return pathField.getPathField().getText();
}
public boolean isFtpPassiveMode() {
return ftpModeCombo.getSelectedItem().equals("Passive");
}
}

View File

@ -12,6 +12,7 @@ import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
@ -44,6 +45,8 @@ public class LibraryEditDialog extends JDialog {
private final JTextField usernameField;
private final JLabel passwordLabel;
private final JPasswordField passwordField;
private final JLabel ftpModeLabel;
private final JComboBox<String> ftpModeCombo;
private final JCheckBox defaultPathCheck;
private final JLabel pathLabel;
private final FileChooserField pathField;
@ -55,7 +58,9 @@ public class LibraryEditDialog extends JDialog {
private final JPanel buttonPanel;
private final JPanel panel;
public LibraryEditDialog(Window parent, String name, String url, String ftp, String username, String password, String file, Runnable cacheClearCallback) {
public LibraryEditDialog(Window parent, String name, String url, String ftp,
String username, String password, boolean passiveMode, String file,
Runnable cacheClearCallback) {
super(parent, "Edit Library");
nameLabel = new JLabel("Name:");
nameField = new JTextField(10);
@ -72,6 +77,9 @@ public class LibraryEditDialog extends JDialog {
passwordLabel = new JLabel("Password:");
passwordField = new JPasswordField(10);
passwordField.setText(password);
ftpModeLabel = new JLabel("FTP Mode:");
ftpModeCombo = new JComboBox<String>(new String[] { "Passive", "Active" });
ftpModeCombo.setSelectedIndex(passiveMode ? 0 : 1);
defaultPathCheck = new JCheckBox("Use default library location");
pathLabel = new JLabel("Library Path:");
pathField = new FileChooserField(10, file, new File(LibrarySelectFrame.DEFAULT_LIBRARY_PATH));
@ -245,11 +253,20 @@ public class LibraryEditDialog extends JDialog {
gbc.gridy = 5;
gbc.gridx = 0;
gbc.fill = GridBagConstraints.NONE;
gbc.anchor = GridBagConstraints.LINE_END;
entryPanel.add(ftpModeLabel, gbc);
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor = GridBagConstraints.LINE_START;
entryPanel.add(ftpModeCombo, gbc);
gbc.gridy = 6;
gbc.gridx = 0;
gbc.fill = GridBagConstraints.NONE;
gbc.anchor = GridBagConstraints.CENTER;
gbc.gridwidth = 2;
entryPanel.add(defaultPathCheck, gbc);
gbc.gridwidth = 1;
gbc.gridy = 6;
gbc.gridy = 7;
gbc.gridx = 0;
gbc.anchor = GridBagConstraints.LINE_END;
entryPanel.add(pathLabel, gbc);
@ -287,4 +304,8 @@ public class LibraryEditDialog extends JDialog {
return pathField.getPathField().getText();
}
public boolean isFtpPassiveMode() {
return ftpModeCombo.getSelectedItem().equals("Passive");
}
}

View File

@ -10,6 +10,7 @@ import java.awt.event.KeyEvent;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
@ -39,6 +40,8 @@ public class LibraryImportDialog extends JDialog {
private final JTextField usernameField;
private final JLabel passwordLabel;
private final JPasswordField passwordField;
private final JLabel ftpModeLabel;
private final JComboBox<String> ftpModeCombo;
private final JLabel fileLabel;
private final FileChooserField fileField;
private final JPanel entryPanel;
@ -58,6 +61,8 @@ public class LibraryImportDialog extends JDialog {
usernameField = new JTextField(10);
passwordLabel = new JLabel("Password:");
passwordField = new JPasswordField(10);
ftpModeLabel = new JLabel("FTP Mode:");
ftpModeCombo = new JComboBox<String>(new String[] { "Passive", "Active" });
fileLabel = new JLabel("Source:");
fileField = new FileChooserField(10);
fileField.getChooser().setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
@ -167,6 +172,15 @@ public class LibraryImportDialog extends JDialog {
gbc.gridx = 0;
gbc.anchor = GridBagConstraints.LINE_END;
gbc.fill = GridBagConstraints.NONE;
entryPanel.add(ftpModeLabel, gbc);
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor = GridBagConstraints.LINE_START;
entryPanel.add(ftpModeCombo, gbc);
gbc.gridy = 5;
gbc.gridx = 0;
gbc.anchor = GridBagConstraints.LINE_END;
gbc.fill = GridBagConstraints.NONE;
entryPanel.add(fileLabel, gbc);
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
@ -198,4 +212,8 @@ public class LibraryImportDialog extends JDialog {
return fileField.getPathField().getText();
}
public boolean isFtpPassiveMode() {
return ftpModeCombo.getSelectedItem().equals("Passive");
}
}

View File

@ -80,12 +80,15 @@ public class LibrarySelectFrame extends JFrame {
public String name;
public String url;
public String ftp;
public boolean passiveMode;
public File file;
public LibraryEntry(String name, String url, String ftp, File file) {
public LibraryEntry(String name, String url, String ftp,
boolean passiveMode, File file) {
this.name = name;
this.url = url;
this.ftp = ftp;
this.passiveMode = passiveMode;
this.file = file;
}
@ -101,7 +104,9 @@ public class LibrarySelectFrame extends JFrame {
public boolean equals(Object o) {
if (o instanceof LibraryEntry) {
LibraryEntry e = (LibraryEntry) o;
return e.name.equals(name) && e.url.equals(url) && e.file.getPath().equals(file.getPath());
return e.name.equals(name) && e.url.equals(url) &&
e.file.getPath().equals(file.getPath()) &&
e.passiveMode == passiveMode;
}
return false;
}
@ -201,7 +206,8 @@ public class LibrarySelectFrame extends JFrame {
nld.setVisible(true);
if (nld.getResponse() == LibraryCreateDialog.RESPONSE_CREATE) {
createLibrary(nld.getLibraryName(), nld.getURL(), nld.getFTP(),
nld.getUsername(), nld.getPassword(), new File(nld.getLibraryFile()));
nld.getUsername(), nld.getPassword(),
nld.isFtpPassiveMode(), new File(nld.getLibraryFile()));
}
});
importItem = new JMenuItem("Import");
@ -209,7 +215,8 @@ public class LibrarySelectFrame extends JFrame {
LibraryImportDialog lid = new LibraryImportDialog(this);
lid.setVisible(true);
if (lid.getResponse() == LibraryImportDialog.RESPONSE_IMPORT) {
importLibrary(lid.getURL(), lid.getFTP(), lid.getUsername(), lid.getPassword(), new File(lid.getFile()));
importLibrary(lid.getURL(), lid.getFTP(), lid.getUsername(),
lid.getPassword(), lid.isFtpPassiveMode(), new File(lid.getFile()));
}
});
listPopup = new JPopupMenu();
@ -287,7 +294,8 @@ public class LibrarySelectFrame extends JFrame {
LibraryImportDialog lid = new LibraryImportDialog(this);
lid.setVisible(true);
if (lid.getResponse() == LibraryImportDialog.RESPONSE_IMPORT) {
importLibrary(lid.getURL(), lid.getFTP(), lid.getUsername(), lid.getPassword(), new File(lid.getFile()));
importLibrary(lid.getURL(), lid.getFTP(), lid.getUsername(),
lid.getPassword(), lid.isFtpPassiveMode(), new File(lid.getFile()));
}
});
newButton = new JButton("New");
@ -297,7 +305,8 @@ public class LibrarySelectFrame extends JFrame {
nld.setVisible(true);
if (nld.getResponse() == LibraryCreateDialog.RESPONSE_CREATE) {
createLibrary(nld.getLibraryName(), nld.getURL(), nld.getFTP(),
nld.getUsername(), nld.getPassword(), new File(nld.getLibraryFile()));
nld.getUsername(), nld.getPassword(),
nld.isFtpPassiveMode(), new File(nld.getLibraryFile()));
}
});
deleteButton = new JButton("Delete");
@ -468,7 +477,9 @@ public class LibrarySelectFrame extends JFrame {
}
private String formatLibraryUrl(String url) {
if (!url.matches("^https?\\:\\/\\/.*")) {
if (url.isBlank()) {
return "";
} else if (!url.matches("^https?\\:\\/\\/.*")) {
return "https://" + url;
}
return url;
@ -476,8 +487,9 @@ public class LibrarySelectFrame extends JFrame {
private void editLibrary(LibraryEntry en) {
Secrets s = SECRETS_FACTORY.createSecrets(SECRETS_KEY + en.name);
LibraryEditDialog led = new LibraryEditDialog(this, en.name, en.url, en.ftp,
s.getUsername(), s.getPassword(), en.file.getPath(),
LibraryEditDialog led = new LibraryEditDialog(this, en.name, en.url,
en.ftp, s.getUsername(), s.getPassword(),
en.passiveMode, en.file.getPath(),
() -> clearLibraryCache(en));
led.setVisible(true);
if (led.getResponse() == LibraryEditDialog.RESPONSE_SAVE) {
@ -506,6 +518,7 @@ public class LibrarySelectFrame extends JFrame {
en.name = led.getLibraryName();
en.url = formatLibraryUrl(led.getURL());
en.ftp = led.getFTP();
en.passiveMode = led.isFtpPassiveMode();
en.file = nf;
saveLibraryList();
list.repaint();
@ -548,7 +561,7 @@ public class LibrarySelectFrame extends JFrame {
settingsPutObject("last-library", en);
Secrets s = SECRETS_FACTORY.createSecrets(SECRETS_KEY + en.name);
try {
Library lib = new Library(en.file, new URI(en.ftp), s);
Library lib = new Library(en.file, new URI(en.ftp), en.passiveMode, s);
LibrarySyncDialog lsd = new LibrarySyncDialog(null, lib);
if (lsd.sync() == LibrarySyncDialog.STATUS_OK) {
return lib;
@ -622,7 +635,8 @@ public class LibrarySelectFrame extends JFrame {
}
}
private void importLibrary(String url, String ftp, String username, String password, File dir) {
private void importLibrary(String url, String ftp, String username,
String password, boolean passiveMode, File dir) {
File cf = null;
try {
cf = dir.getCanonicalFile();
@ -640,7 +654,7 @@ public class LibrarySelectFrame extends JFrame {
Secrets s = SECRETS_FACTORY.createSecrets(SECRETS_KEY + name);
s.setUsername(username);
s.setPassword(password);
LibraryEntry en = new LibraryEntry(name, url, ftp, cf);
LibraryEntry en = new LibraryEntry(name, url, ftp, passiveMode, cf);
LIBRARY_ENTRIES.add(0, en);
openLibrary(en, false);
}
@ -702,7 +716,8 @@ public class LibrarySelectFrame extends JFrame {
return true;
}
private void createLibrary(String name, String url, String ftp, String username, String password, File file) {
private void createLibrary(String name, String url, String ftp,
String username, String password, boolean passiveMode, File file) {
File cf = null;
try {
cf = file.getCanonicalFile();
@ -726,7 +741,7 @@ public class LibrarySelectFrame extends JFrame {
recursiveDelete(cf);
break;
case LibraryExistsDialog.RESPONSE_IMPORT:
importLibrary(url, ftp, username, password, file);
importLibrary(url, ftp, username, password, passiveMode, file);
return;
default:
return;
@ -756,7 +771,7 @@ public class LibrarySelectFrame extends JFrame {
Secrets s = SECRETS_FACTORY.createSecrets(SECRETS_KEY + name);
s.setUsername(username);
s.setPassword(password);
LibraryEntry en = new LibraryEntry(name, url, ftp, cf);
LibraryEntry en = new LibraryEntry(name, url, ftp, passiveMode, cf);
LIBRARY_ENTRIES.add(0, en);
openLibrary(en, false);
}

View File

@ -3,6 +3,8 @@ package zander.ui.media.map;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
@ -128,9 +130,12 @@ public class OSMLocationEncoder implements LocationEncoder {
private static URL getURLForQuery(String query) {
try {
final String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8);
return new URL("https://nominatim.openstreetmap.org/search.php?format=jsonv2&q=" + encodedQuery);
} catch (MalformedURLException e) {
final String encodedQuery = URLEncoder.encode(query,
StandardCharsets.UTF_8);
return new URI(
"https://nominatim.openstreetmap.org/search.php?format=jsonv2&q=" +
encodedQuery).toURL();
} catch (URISyntaxException | MalformedURLException e) {
LOGGER.error("Could not generate url for query: '{}'", query, e);
throw new Error("Could not create URL!", e);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -4,18 +4,22 @@
<h1 style="text-align: center">Local Library Settings</h1>
<h2>Overview</h2>
<p>
These dialogs create, import or alter the settings of libraries. These settings are primarily
for use of Curator and will not affect the Art Museum website.
These dialogs create, import or alter the settings of libraries. These
settings are primarily for use of Curator and will not affect the Art
Museum website.
</p>
<p>
<b>Local vs Remote Library</b>
<br>
Remore libraries are instances of the Art Museum website. They are connected to by FTP.
Any changes made to them will be reflected in their respective Art Museum instance.
Remore libraries are instances of the Art Museum website. They are
connected to by FTP. Any changes made to them will be reflected in their
respective Art Museum instance.
<br>
Local libraries, or caches, are instances of libraries located on your computer. When you connect
to a remote library, a local library is automatically created and sync with its remote counterpart.
Every time you connect to a remote library, its local counterpart is updated in this way.
Local libraries, or caches, are instances of libraries located on your
computer. When you connect to a remote library, a local library is
automatically created and sync with its remote counterpart. Every time
you connect to a remote library, its local counterpart is updated in this
way.
</p>
<img width="500" src="library-create-dialog.png"></img>
<p>The library creation and remote import dialog.</p>
@ -29,16 +33,25 @@
<p>The local library import dialog.</p>
<h2>Options</h2>
<ul>
<li><b>Name</b>: library name in the select dialog. Does not affect website.</li>
<li><b>URL</b>: url in the select dialog. Can be blank. Does not affect the website.</li>
<li><b>FTP</b>: FTP server to connect to. Must be in the format of 'ftp://hostname:port/path/to/library'.
Protocol can also be FTPS by replacing 'ftp://' with 'ftps://'.</li>
<li><b>Username & Password</b>: username and password to use to connect to FTP server.</li>
<li><b>Library Location (Source)</b>: location to save caches in. If 'User default library location'
is checked, it will be stored in the directory set in the global settings menu.
<li><b>Name</b>: library name in the select dialog. Does not affect
website.</li>
<li><b>URL</b>: url in the select dialog. Can be blank. Does not affect
the website.</li>
<li><b>FTP</b>: FTP server to connect to. Must be in the format of
'ftp://hostname:port/path/to/library'. Encryption is supported via
FTPS. To enable it, replace 'ftp://' with 'ftps://'.</li>
<li><b>Username & Password</b>: username and password to use to connect to
FTP server.</li>
<li><b>FTP Mode</b>: Whether to use active (PORT) or passive (PASV) FTP
mode. If you are unsure, it's best to start with passive and change to
active if you are having trouble connecting.</li>
<li><b>Library Location (Source)</b>: location to save caches in. If 'User
default library location' is checked, it will be stored in the directory
set in the global settings menu.
<a href="help:Libraries/Global Settings">See: Global Settings</a></li>
<li><b>Clear Cahce</b>: clear library cahce. This does <b>NOT</b> affect the remote library. It only
clears the local cache. Doing this will cause a full re-download of the remote library next time it is
<li><b>Clear Cahce</b>: clear library cahce. This does <b>NOT</b> affect
the remote library. It only clears the local cache. Doing this will
cause a full re-download of the remote library next time it is
oppened.</li>
</ul>
</body>