/* * Copyright (C) 2008, Shawn O. Pearce and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.transport; import static org.eclipse.jgit.lib.Constants.INFO_ALTERNATES; import static org.eclipse.jgit.lib.Constants.LOCK_SUFFIX; import static org.eclipse.jgit.lib.Constants.OBJECTS; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref.Storage; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.SymbolicRef; /** * Transport over the non-Git aware SFTP (SSH based FTP) protocol. *

* The SFTP transport does not require any specialized Git support on the remote * (server side) repository. Object files are retrieved directly through secure * shell's FTP protocol, making it possible to copy objects from a remote * repository that is available over SSH, but whose remote host does not have * Git installed. *

* Unlike the HTTP variant (see * {@link org.eclipse.jgit.transport.TransportHttp}) we rely upon being able to * list files in directories, as the SFTP protocol supports this function. By * listing files through SFTP we can avoid needing to have current * objects/info/packs or info/refs files on the remote * repository and access the data directly, much as Git itself would. *

* Concurrent pushing over this transport is not supported. Multiple concurrent * push operations may cause confusion in the repository state. * * @see WalkFetchConnection */ public class TransportSftp extends SshTransport implements WalkTransport { static final TransportProtocol PROTO_SFTP = new TransportProtocol() { @Override public String getName() { return JGitText.get().transportProtoSFTP; } @Override public Set getSchemes() { return Collections.singleton("sftp"); //$NON-NLS-1$ } @Override public Set getRequiredFields() { return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST, URIishField.PATH)); } @Override public Set getOptionalFields() { return Collections.unmodifiableSet(EnumSet.of(URIishField.USER, URIishField.PASS, URIishField.PORT)); } @Override public int getDefaultPort() { return 22; } @Override public Transport open(URIish uri, Repository local, String remoteName) throws NotSupportedException { return new TransportSftp(local, uri); } }; TransportSftp(Repository local, URIish uri) { super(local, uri); } @Override public FetchConnection openFetch() throws TransportException { final SftpObjectDB c = new SftpObjectDB(uri.getPath()); final WalkFetchConnection r = new WalkFetchConnection(this, c); r.available(c.readAdvertisedRefs()); return r; } @Override public PushConnection openPush() throws TransportException { final SftpObjectDB c = new SftpObjectDB(uri.getPath()); final WalkPushConnection r = new WalkPushConnection(this, c); r.available(c.readAdvertisedRefs()); return r; } FtpChannel newSftp() throws IOException { FtpChannel channel = getSession().getFtpChannel(); channel.connect(getTimeout(), TimeUnit.SECONDS); return channel; } class SftpObjectDB extends WalkRemoteObjectDatabase { private final String objectsPath; private FtpChannel ftp; SftpObjectDB(String path) throws TransportException { if (path.startsWith("/~")) //$NON-NLS-1$ path = path.substring(1); if (path.startsWith("~/")) //$NON-NLS-1$ path = path.substring(2); try { ftp = newSftp(); ftp.cd(path); ftp.cd(OBJECTS); objectsPath = ftp.pwd(); } catch (FtpChannel.FtpException f) { throw new TransportException(MessageFormat.format( JGitText.get().cannotEnterObjectsPath, path, f.getMessage()), f); } catch (IOException ioe) { close(); throw new TransportException(uri, ioe.getMessage(), ioe); } } SftpObjectDB(SftpObjectDB parent, String p) throws TransportException { try { ftp = newSftp(); ftp.cd(parent.objectsPath); ftp.cd(p); objectsPath = ftp.pwd(); } catch (FtpChannel.FtpException f) { throw new TransportException(MessageFormat.format( JGitText.get().cannotEnterPathFromParent, p, parent.objectsPath, f.getMessage()), f); } catch (IOException ioe) { close(); throw new TransportException(uri, ioe.getMessage(), ioe); } } @Override URIish getURI() { return uri.setPath(objectsPath); } @Override Collection getAlternates() throws IOException { try { return readAlternates(INFO_ALTERNATES); } catch (FileNotFoundException err) { return null; } } @Override WalkRemoteObjectDatabase openAlternate(String location) throws IOException { return new SftpObjectDB(this, location); } @Override Collection getPackNames() throws IOException { final List packs = new ArrayList<>(); try { Collection list = ftp.ls("pack"); //$NON-NLS-1$ Set files = list.stream() .map(FtpChannel.DirEntry::getFilename) .collect(Collectors.toSet()); HashMap mtimes = new HashMap<>(); for (FtpChannel.DirEntry ent : list) { String n = ent.getFilename(); if (!n.startsWith("pack-") || !n.endsWith(".pack")) { //$NON-NLS-1$ //$NON-NLS-2$ continue; } String in = n.substring(0, n.length() - 5) + ".idx"; //$NON-NLS-1$ if (!files.contains(in)) { continue; } mtimes.put(n, Long.valueOf(ent.getModifiedTime())); packs.add(n); } Collections.sort(packs, (o1, o2) -> mtimes.get(o2).compareTo(mtimes.get(o1))); } catch (FtpChannel.FtpException f) { throw new TransportException( MessageFormat.format(JGitText.get().cannotListPackPath, objectsPath, f.getMessage()), f); } return packs; } @Override FileStream open(String path) throws IOException { try { return new FileStream(ftp.get(path)); } catch (FtpChannel.FtpException f) { if (f.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) { throw new FileNotFoundException(path); } throw new TransportException(MessageFormat.format( JGitText.get().cannotGetObjectsPath, objectsPath, path, f.getMessage()), f); } } @Override void deleteFile(String path) throws IOException { try { ftp.delete(path); } catch (FtpChannel.FtpException f) { throw new TransportException(MessageFormat.format( JGitText.get().cannotDeleteObjectsPath, objectsPath, path, f.getMessage()), f); } // Prune any now empty directories. // String dir = path; int s = dir.lastIndexOf('/'); while (s > 0) { try { dir = dir.substring(0, s); ftp.rmdir(dir); s = dir.lastIndexOf('/'); } catch (IOException je) { // If we cannot delete it, leave it alone. It may have // entries still in it, or maybe we lack write access on // the parent. Either way it isn't a fatal error. // break; } } } @Override OutputStream writeFile(String path, ProgressMonitor monitor, String monitorTask) throws IOException { Throwable err = null; try { return ftp.put(path); } catch (FileNotFoundException e) { mkdir_p(path); } catch (FtpChannel.FtpException je) { if (je.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) { mkdir_p(path); } else { err = je; } } if (err == null) { try { return ftp.put(path); } catch (IOException e) { err = e; } } throw new TransportException( MessageFormat.format(JGitText.get().cannotWriteObjectsPath, objectsPath, path, err.getMessage()), err); } @Override void writeFile(String path, byte[] data) throws IOException { final String lock = path + LOCK_SUFFIX; try { super.writeFile(lock, data); try { ftp.rename(lock, path); } catch (IOException e) { throw new TransportException(MessageFormat.format( JGitText.get().cannotWriteObjectsPath, objectsPath, path, e.getMessage()), e); } } catch (IOException err) { try { ftp.rm(lock); } catch (IOException e) { // Ignore deletion failure, we are already // failing anyway. } throw err; } } private void mkdir_p(String path) throws IOException { final int s = path.lastIndexOf('/'); if (s <= 0) return; path = path.substring(0, s); Throwable err = null; try { ftp.mkdir(path); return; } catch (FileNotFoundException f) { mkdir_p(path); } catch (FtpChannel.FtpException je) { if (je.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) { mkdir_p(path); } else { err = je; } } if (err == null) { try { ftp.mkdir(path); return; } catch (IOException e) { err = e; } } throw new TransportException(MessageFormat.format( JGitText.get().cannotMkdirObjectPath, objectsPath, path, err.getMessage()), err); } Map readAdvertisedRefs() throws TransportException { final TreeMap avail = new TreeMap<>(); readPackedRefs(avail); readRef(avail, ROOT_DIR + Constants.HEAD, Constants.HEAD); readLooseRefs(avail, ROOT_DIR + "refs", "refs/"); //$NON-NLS-1$ //$NON-NLS-2$ return avail; } private void readLooseRefs(TreeMap avail, String dir, String prefix) throws TransportException { final Collection list; try { list = ftp.ls(dir); } catch (IOException e) { throw new TransportException(MessageFormat.format( JGitText.get().cannotListObjectsPath, objectsPath, dir, e.getMessage()), e); } for (FtpChannel.DirEntry ent : list) { String n = ent.getFilename(); if (".".equals(n) || "..".equals(n)) //$NON-NLS-1$ //$NON-NLS-2$ continue; String nPath = dir + "/" + n; //$NON-NLS-1$ if (ent.isDirectory()) { readLooseRefs(avail, nPath, prefix + n + "/"); //$NON-NLS-1$ } else { readRef(avail, nPath, prefix + n); } } } private Ref readRef(TreeMap avail, String path, String name) throws TransportException { final String line; try (BufferedReader br = openReader(path)) { line = br.readLine(); } catch (FileNotFoundException noRef) { return null; } catch (IOException err) { throw new TransportException(MessageFormat.format( JGitText.get().cannotReadObjectsPath, objectsPath, path, err.getMessage()), err); } if (line == null) { throw new TransportException( MessageFormat.format(JGitText.get().emptyRef, name)); } if (line.startsWith("ref: ")) { //$NON-NLS-1$ final String target = line.substring("ref: ".length()); //$NON-NLS-1$ Ref r = avail.get(target); if (r == null) r = readRef(avail, ROOT_DIR + target, target); if (r == null) r = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null); r = new SymbolicRef(name, r); avail.put(r.getName(), r); return r; } if (ObjectId.isId(line)) { final Ref r = new ObjectIdRef.Unpeeled(loose(avail.get(name)), name, ObjectId.fromString(line)); avail.put(r.getName(), r); return r; } throw new TransportException( MessageFormat.format(JGitText.get().badRef, name, line)); } private Storage loose(Ref r) { if (r != null && r.getStorage() == Storage.PACKED) { return Storage.LOOSE_PACKED; } return Storage.LOOSE; } @Override void close() { if (ftp != null) { try { if (ftp.isConnected()) { ftp.disconnect(); } } finally { ftp = null; } } } } }