Mercurial > emacs
diff lisp/net/tramp-smb.el @ 48973:09acf3f65bb5
* net/tramp*.el: Sync with upstream version 2.0.28. Bugfixes.
* net/tramp-ftp.el: Glue code with Ange-FTP, broken out of
tramp.el. From Michael Albinus.
* net/tramp-smb.el: New file for using smbclient to access
Windows shares with Tramp. From Michael Albinus.
author | Kai Großjohann <kgrossjo@eu.uu.net> |
---|---|
date | Thu, 26 Dec 2002 20:47:51 +0000 |
parents | |
children | 0d8b17d428b5 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lisp/net/tramp-smb.el Thu Dec 26 20:47:51 2002 +0000 @@ -0,0 +1,1102 @@ +;;; tramp-smb.el --- Tramp access functions for SMB servers -*- coding: iso-8859-1; -*- + +;; Copyright (C) 2002 Free Software Foundation, Inc. + +;; Author: Michael Albinus <Michael.Albinus@alcatel.de> +;; Keywords: comm, processes + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 2, or (at your option) +;; any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; Access functions for SMB servers like SAMBA or M$ Windows from Tramp. + +;;; Code: + +(require 'tramp) + +;; Pacify byte-compiler +(eval-when-compile + (require 'cl) + (require 'custom) + ;; Emacs 19.34 compatibility hack -- is this needed? + (or (>= emacs-major-version 20) + (load "cl-seq"))) + +;; Define SMB method ... +(defcustom tramp-smb-method "smb" + "*Method to connect SAMBA and M$ SMB servers." + :group 'tramp + :type 'string) + +;; ... and add it to the method list. +(add-to-list 'tramp-methods (cons tramp-smb-method nil)) + +;; Add a default for `tramp-default-method-alist'. Rule: If there is +;; a domain in USER, it must be the SMB method. +(add-to-list 'tramp-default-method-alist + '("%" "" tramp-smb-method)) + +;; Add completion function for SMB method. +(tramp-set-completion-function + tramp-smb-method + '((tramp-parse-netrc "~/.netrc"))) + +(defcustom tramp-smb-program "smbclient" + "*Name of SMB client to run." + :group 'tramp + :type 'string) + +(defconst tramp-smb-prompt "^smb: \\S-+> " + "Regexp used as prompt in smbclient.") + +(defconst tramp-smb-errors + (mapconcat + 'identity + '(; Connection error + "Connection to \\S-+ failed" + ; Samba + "ERRSRV" + "ERRDOS" + "ERRbadfile" + "ERRbadpw" + "ERRfilexists" + "ERRnoaccess" + "ERRnomem" + "ERRnosuchshare" + ; Windows NT 4.0, Windows 5.0 (Windows 2000), Windows 5.1 (Windows XP) + "NT_STATUS_ACCESS_DENIED" + "NT_STATUS_BAD_NETWORK_NAME" + "NT_STATUS_CANNOT_DELETE" + "NT_STATUS_LOGON_FAILURE" + "NT_STATUS_NO_SUCH_FILE" + "NT_STATUS_OBJECT_NAME_INVALID" + "NT_STATUS_OBJECT_NAME_NOT_FOUND" + "NT_STATUS_SHARING_VIOLATION") + "\\|") + "Regexp for possible error strings of SMB servers. +Used instead of analyzing error codes of commands.") + +(defvar tramp-smb-share nil + "Holds the share name for the current buffer. +This variable is local to each buffer.") +(make-variable-buffer-local 'tramp-smb-share) + +(defvar tramp-smb-share-cache nil + "Caches the share names accessible to host related to the current buffer. +This variable is local to each buffer.") +(make-variable-buffer-local 'tramp-smb-share-cache) + +(defvar tramp-smb-process-running nil + "Flag whether a corresponding process is still running. +Will be changed by corresponding `process-sentinel'. +This variable is local to each buffer.") +(make-variable-buffer-local 'tramp-smb-process-running) + +;; New handlers should be added here. +(defconst tramp-smb-file-name-handler-alist + '( + ;; `access-file' performed by default handler + (add-name-to-file . tramp-smb-handle-copy-file) ;; we're on Windows, honey. + ;; `byte-compiler-base-file-name' performed by default handler + (copy-file . tramp-smb-handle-copy-file) + (delete-directory . tramp-smb-handle-delete-directory) + (delete-file . tramp-smb-handle-delete-file) + ;; `diff-latest-backup-file' performed by default handler + ;; `directory-file-name' performed by default handler + (directory-files . tramp-smb-handle-directory-files) + (directory-files-and-attributes . tramp-smb-handle-directory-files-and-attributes) + (dired-call-process . tramp-smb-not-handled) + (dired-compress-file . tramp-smb-not-handled) + ;; `dired-uncache' performed by default handler + ;; `expand-file-name' not necessary because we cannot expand "~/" + (file-accessible-directory-p . tramp-smb-handle-file-directory-p) + (file-attributes . tramp-smb-handle-file-attributes) + (file-directory-p . tramp-smb-handle-file-directory-p) + (file-executable-p . tramp-smb-handle-file-exists-p) + (file-exists-p . tramp-smb-handle-file-exists-p) + (file-local-copy . tramp-smb-handle-file-local-copy) + (file-modes . tramp-handle-file-modes) + (file-name-all-completions . tramp-smb-handle-file-name-all-completions) + ;; `file-name-as-directory' performed by default handler + (file-name-completion . tramp-handle-file-name-completion) + (file-name-directory . tramp-handle-file-name-directory) + (file-name-nondirectory . tramp-handle-file-name-nondirectory) + ;; `file-name-sans-versions' performed by default handler + (file-newer-than-file-p . tramp-smb-handle-file-newer-than-file-p) + (file-ownership-preserved-p . tramp-smb-not-handled) + (file-readable-p . tramp-smb-handle-file-exists-p) + (file-regular-p . tramp-handle-file-regular-p) + (file-symlink-p . tramp-smb-not-handled) + ;; `file-truename' performed by default handler + (file-writable-p . tramp-smb-handle-file-writable-p) + ;; `find-backup-file-name' performed by default handler + ;; `find-file-noselect' performed by default handler + ;; `get-file-buffer' performed by default handler + (insert-directory . tramp-smb-handle-insert-directory) + (insert-file-contents . tramp-handle-insert-file-contents) + (load . tramp-handle-load) + (make-directory . tramp-smb-handle-make-directory) + (make-directory-internal . tramp-smb-handle-make-directory-internal) + (make-symbolic-link . tramp-smb-not-handled) + (rename-file . tramp-smb-handle-rename-file) + (set-file-modes . tramp-smb-not-handled) + (set-visited-file-modtime . tramp-smb-not-handled) + (shell-command . tramp-smb-not-handled) + ;; `substitute-in-file-name' performed by default handler + (unhandled-file-name-directory . tramp-handle-unhandled-file-name-directory) + (vc-registered . tramp-smb-not-handled) + (verify-visited-file-modtime . tramp-smb-not-handled) + (write-region . tramp-smb-handle-write-region) +) + "Alist of handler functions for Tramp SMB method. +Operations not mentioned here will be handled by the default Emacs primitives.") + +(defun tramp-smb-file-name-p (filename) + "Check if it's a filename for SMB servers." + (let ((v (tramp-dissect-file-name filename))) + (string= + (tramp-find-method + (tramp-file-name-multi-method v) + (tramp-file-name-method v) + (tramp-file-name-user v) + (tramp-file-name-host v)) + tramp-smb-method))) + +(defun tramp-smb-file-name-handler (operation &rest args) + "Invoke the SMB related OPERATION. +First arg specifies the OPERATION, second arg is a list of arguments to +pass to the OPERATION." + (let ((fn (assoc operation tramp-smb-file-name-handler-alist))) + (if fn + (if (eq (cdr fn) 'tramp-smb-not-handled) + (apply (cdr fn) operation args) + (save-match-data (apply (cdr fn) args))) + (tramp-run-real-handler operation args)))) + +(add-to-list 'tramp-foreign-file-name-handler-alist + (cons 'tramp-smb-file-name-p 'tramp-smb-file-name-handler)) + + +;; File name primitives + +(defun tramp-smb-not-handled (operation &rest args) + "Default handler for all functions which are disrecarded." + (tramp-message 10 "Won't be handled: %s %s" operation args) + nil) + +(defun tramp-smb-handle-copy-file + (filename newname &optional ok-if-already-exists keep-date) + "Like `copy-file' for tramp files. +KEEP-DATE is not handled in case NEWNAME resides on an SMB server." + (setq filename (expand-file-name filename) + newname (expand-file-name newname)) + + (let ((tmpfile (file-local-copy filename))) + + (if tmpfile + ;; remote filename + (rename-file tmpfile newname ok-if-already-exists) + + ;; remote newname + (when (file-directory-p newname) + (setq newname (expand-file-name + (file-name-nondirectory filename) newname))) + (when (and (not ok-if-already-exists) + (file-exists-p newname)) + (error "copy-file: file %s already exists" newname)) + +; (with-parsed-tramp-file-name newname nil + (let (user host path) + (with-parsed-tramp-file-name newname l + (setq user l-user host l-host path l-path)) + (save-excursion + (let ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path t))) + (unless share + (error "Target `%s' must contain a share name" filename)) + (tramp-smb-maybe-open-connection user host share) + (tramp-message-for-buffer + nil tramp-smb-method user host + 5 "Copying file %s to file %s..." filename newname) + (if (tramp-smb-send-command + user host (format "put %s \"%s\"" filename file)) + (tramp-message-for-buffer + nil tramp-smb-method user host + 5 "Copying file %s to file %s...done" filename newname) + (error "Cannot copy `%s'" filename)))))))) + +(defun tramp-smb-handle-delete-directory (directory) + "Like `delete-directory' for tramp files." + (setq directory (directory-file-name (expand-file-name directory))) + (unless (file-exists-p directory) + (error "Cannot delete non-existing directory `%s'" directory)) +; (with-parsed-tramp-file-name directory nil + (let (user host path) + (with-parsed-tramp-file-name directory l + (setq user l-user host l-host path l-path)) + (save-excursion + (let ((share (tramp-smb-get-share path)) + (dir (tramp-smb-get-path (file-name-directory path) t)) + (file (file-name-nondirectory path))) + (tramp-smb-maybe-open-connection user host share) + (if (and + (tramp-smb-send-command user host (format "cd \"%s\"" dir)) + (tramp-smb-send-command user host (format "rmdir \"%s\"" file))) + ;; Go Home + (tramp-smb-send-command user host (format "cd \\")) + ;; Error + (tramp-smb-send-command user host (format "cd \\")) + (error "Cannot delete directory `%s'" directory)))))) + +(defun tramp-smb-handle-delete-file (filename) + "Like `delete-file' for tramp files." + (setq filename (expand-file-name filename)) + (unless (file-exists-p filename) + (error "Cannot delete non-existing file `%s'" filename)) +; (with-parsed-tramp-file-name filename nil + (let (user host path) + (with-parsed-tramp-file-name filename l + (setq user l-user host l-host path l-path)) + (save-excursion + (let ((share (tramp-smb-get-share path)) + (dir (tramp-smb-get-path (file-name-directory path) t)) + (file (file-name-nondirectory path))) + (unless (file-exists-p filename) + (error "Cannot delete non-existing file `%s'" filename)) + (tramp-smb-maybe-open-connection user host share) + (if (and + (tramp-smb-send-command user host (format "cd \"%s\"" dir)) + (tramp-smb-send-command user host (format "rm \"%s\"" file))) + ;; Go Home + (tramp-smb-send-command user host (format "cd \\")) + ;; Error + (tramp-smb-send-command user host (format "cd \\")) + (error "Cannot delete file `%s'" directory)))))) + +(defun tramp-smb-handle-directory-files + (directory &optional full match nosort) + "Like `directory-files' for tramp files." + (setq directory (directory-file-name (expand-file-name directory))) +; (with-parsed-tramp-file-name directory nil + (let (user host path) + (with-parsed-tramp-file-name directory l + (setq user l-user host l-host path l-path)) + (save-excursion + (let* ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path nil)) + (entries (tramp-smb-get-file-entries user host share file))) + ;; Just the file names are needed + (setq entries (mapcar 'car entries)) + ;; Discriminate with regexp + (when match + (setq entries + (delete nil + (mapcar (lambda (x) (when (string-match match x) x)) + entries)))) + ;; Make absolute paths if necessary + (when full + (setq entries + (mapcar (lambda (x) + (concat (file-name-as-directory directory) x)) + entries))) + ;; Sort them if necessary + (unless nosort (setq entries (sort entries 'string-lessp))) + ;; That's it + entries)))) + +(defun tramp-smb-handle-directory-files-and-attributes + (directory &optional full match nosort) + "Like `directory-files-and-attributes' for tramp files." + (mapcar + (lambda (x) + (cons x (file-attributes + (if full x (concat (file-name-as-directory directory) x))))) + (directory-files directory full match nosort))) + +(defun tramp-smb-handle-file-attributes (filename &optional nonnumeric) + "Like `file-attributes' for tramp files. +Optional argument NONNUMERIC means return user and group name +rather than as numbers." +; (with-parsed-tramp-file-name filename nil + (let (user host path) + (with-parsed-tramp-file-name filename l + (setq user l-user host l-host path l-path)) + (save-excursion + (let* ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path nil)) + (entries (tramp-smb-get-file-entries user host share file)) + (entry (and entries + (assoc (file-name-nondirectory file) entries)))) + ; check result + (when entry + (list (and (string-match "d" (nth 1 entry)) + t) ;0 file type + -1 ;1 link count + -1 ;2 uid + -1 ;3 gid + (nth 3 entry) ;4 atime + (nth 3 entry) ;5 mtime + (nth 3 entry) ;6 ctime + (nth 2 entry) ;7 size + (nth 1 entry) ;8 mode + nil ;9 gid weird + -1 ;10 inode number + -1)))))) ;11 file system number + +(defun tramp-smb-handle-file-directory-p (filename) + "Like `file-directory-p' for tramp files." +; (with-parsed-tramp-file-name filename nil + (let (user host path) + (with-parsed-tramp-file-name filename l + (setq user l-user host l-host path l-path)) + (save-excursion + (let* ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path nil)) + (entries (tramp-smb-get-file-entries user host share file)) + (entry (and entries + (assoc (file-name-nondirectory file) entries)))) + (and entry + (string-match "d" (nth 1 entry)) + t))))) + +(defun tramp-smb-handle-file-exists-p (filename) + "Like `file-exists-p' for tramp files." +; (with-parsed-tramp-file-name filename nil + (let (user host path) + (with-parsed-tramp-file-name filename l + (setq user l-user host l-host path l-path)) + (save-excursion + (let* ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path nil)) + (entries (tramp-smb-get-file-entries user host share file))) + (and entries + (member (file-name-nondirectory file) (mapcar 'car entries)) + t))))) + +(defun tramp-smb-handle-file-local-copy (filename) + "Like `file-local-copy' for tramp files." + (with-parsed-tramp-file-name filename nil + (save-excursion + (let ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path t)) + (tmpfil (tramp-make-temp-file))) + (unless (file-exists-p filename) + (error "Cannot make local copy of non-existing file `%s'" filename)) + (tramp-message-for-buffer + nil tramp-smb-method user host + 5 "Fetching %s to tmp file %s..." filename tmpfil) + (tramp-smb-maybe-open-connection user host share) + (if (tramp-smb-send-command + user host (format "get \"%s\" %s" file tmpfil)) + (tramp-message-for-buffer + nil tramp-smb-method user host + 5 "Fetching %s to tmp file %s...done" filename tmpfil) + (error "Cannot make local copy of file `%s'" filename)) + tmpfil)))) + +;; This function should return "foo/" for directories and "bar" for +;; files. +(defun tramp-smb-handle-file-name-all-completions (filename directory) + "Like `file-name-all-completions' for tramp files." +; (with-parsed-tramp-file-name directory nil + (let (user host path) + (with-parsed-tramp-file-name directory l + (setq user l-user host l-host path l-path)) + (save-match-data + (save-excursion + (let* ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path nil)) + (entries (tramp-smb-get-file-entries user host share file))) + + (all-completions + filename + (mapcar + (lambda (x) + (list + (if (string-match "d" (nth 1 x)) + (file-name-as-directory (nth 0 x)) + (nth 0 x)))) + entries))))))) + +(defun tramp-smb-handle-file-newer-than-file-p (file1 file2) + "Like `file-newer-than-file-p' for tramp files." + (cond + ((not (file-exists-p file1)) nil) + ((not (file-exists-p file2)) t) + (t (tramp-smb-time-less-p (file-attributes file2) + (file-attributes file1))))) + +(defun tramp-smb-handle-file-writable-p (filename) + "Like `file-writable-p' for tramp files." +; (with-parsed-tramp-file-name filename nil + (let (user host path) + (with-parsed-tramp-file-name filename l + (setq user l-user host l-host path l-path)) + (save-excursion + (let* ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path nil)) + (entries (tramp-smb-get-file-entries user host share file)) + (entry (and entries + (assoc (file-name-nondirectory file) entries)))) + (and entry + (string-match "w" (nth 1 entry)) + t))))) + +(defun tramp-smb-handle-insert-directory + (filename switches &optional wildcard full-directory-p) + "Like `insert-directory' for tramp files. +WILDCARD and FULL-DIRECTORY-P are not handled." + (setq filename (expand-file-name filename)) + (when (file-directory-p filename) + ;; This check is a little bit strange, but in `dired-add-entry' + ;; this function is called with a non-directory ... + (setq filename (file-name-as-directory filename))) +; (with-parsed-tramp-file-name filename nil + (let (user host path) + (with-parsed-tramp-file-name filename l + (setq user l-user host l-host path l-path)) + (save-match-data + (let* ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path nil)) + (entries (tramp-smb-get-file-entries user host share file))) + + ;; Delete dummy "" entry, useless entries + (setq entries + (if (file-directory-p filename) + (delq (assoc "" entries) entries) + ;; We just need the only and only entry FILENAME. + (list (assoc (file-name-nondirectory filename) entries)))) + + ;; Sort entries + (setq entries + (sort + entries + (lambda (x y) + (if (string-match "t" switches) + ; sort by date + (tramp-smb-time-less-p (nth 3 y) (nth 3 x)) + ; sort by name + (string-lessp (nth 0 x) (nth 0 y)))))) + + ;; Print entries + (mapcar + (lambda (x) + (insert + (format + "%10s %3d %-8s %-8s %8s %s %s\n" + (nth 1 x) ; mode + 1 "nobody" "nogroup" + (nth 2 x) ; size + (format-time-string + (if (tramp-smb-time-less-p + (tramp-smb-time-subtract (current-time) (nth 3 x)) + tramp-smb-half-a-year) + "%b %e %R" + "%b %e %Y") + (nth 3 x)) ; date + (nth 0 x))) ; file name + (forward-line) + (beginning-of-line)) + entries))))) + +(defun tramp-smb-handle-make-directory (dir &optional parents) + "Like `make-directory' for tramp files." + (setq dir (directory-file-name (expand-file-name dir))) + (unless (file-name-absolute-p dir) + (setq dir (concat default-directory dir))) +; (with-parsed-tramp-file-name dir nil + (let (user host path) + (with-parsed-tramp-file-name dir l + (setq user l-user host l-host path l-path)) + (save-match-data + (let* ((share (tramp-smb-get-share path)) + (ldir (file-name-directory dir))) + ;; Make missing directory parts + (when (and parents share (not (file-directory-p ldir))) + (make-directory ldir parents)) + ;; Just do it + (when (file-directory-p ldir) + (tramp-smb-handle-make-directory-internal dir)) + (unless (file-directory-p dir) + (error "Couldn't make directory %s" dir)))))) + +(defun tramp-smb-handle-make-directory-internal (directory) + "Like `make-directory-internal' for tramp files." + (setq directory (directory-file-name (expand-file-name directory))) + (unless (file-name-absolute-p directory) + (setq ldir (concat default-directory directory))) +; (with-parsed-tramp-file-name directory nil + (let (user host path) + (with-parsed-tramp-file-name directory l + (setq user l-user host l-host path l-path)) + (save-match-data + (let* ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path nil))) + (when (file-directory-p (file-name-directory directory)) + (tramp-smb-maybe-open-connection user host share) + (tramp-smb-send-command user host (format "mkdir \"%s\"" file))) + (unless (file-directory-p directory) + (error "Couldn't make directory %s" directory)))))) + +(defun tramp-smb-handle-rename-file + (filename newname &optional ok-if-already-exists) + "Like `rename-file' for tramp files." + (setq filename (expand-file-name filename) + newname (expand-file-name newname)) + + (let ((tmpfile (file-local-copy filename))) + + (if tmpfile + ;; remote filename + (rename-file tmpfile newname ok-if-already-exists) + + ;; remote newname + (when (file-directory-p newname) + (setq newname (expand-file-name + (file-name-nondirectory filename) newname))) + (when (and (not ok-if-already-exists) + (file-exists-p newname)) + (error "rename-file: file %s already exists" newname)) + +; (with-parsed-tramp-file-name newname nil + (let (user host path) + (with-parsed-tramp-file-name newname l + (setq user l-user host l-host path l-path)) + (save-excursion + (let ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path t))) + (tramp-smb-maybe-open-connection user host share) + (tramp-message-for-buffer + nil tramp-smb-method user host + 5 "Copying file %s to file %s..." filename newname) + (if (tramp-smb-send-command + user host (format "put %s \"%s\"" filename file)) + (tramp-message-for-buffer + nil tramp-smb-method user host + 5 "Copying file %s to file %s...done" filename newname) + (error "Cannot rename `%s'" filename))))))) + + (delete-file filename)) + +(defun tramp-smb-handle-write-region + (start end filename &optional append visit lockname confirm) + "Like `write-region' for tramp files." + (unless (eq append nil) + (error "Cannot append to file using tramp (`%s')" filename)) + (setq filename (expand-file-name filename)) + ;; XEmacs takes a coding system as the seventh argument, not `confirm' + (when (and (not (featurep 'xemacs)) + confirm (file-exists-p filename)) + (unless (y-or-n-p (format "File %s exists; overwrite anyway? " + filename)) + (error "File not overwritten"))) +; (with-parsed-tramp-file-name filename nil + (let (user host path) + (with-parsed-tramp-file-name filename l + (setq user l-user host l-host path l-path)) + (save-excursion + (let ((share (tramp-smb-get-share path)) + (file (tramp-smb-get-path path t)) + (curbuf (current-buffer)) + ;; We use this to save the value of `last-coding-system-used' + ;; after writing the tmp file. At the end of the function, + ;; we set `last-coding-system-used' to this saved value. + ;; This way, any intermediary coding systems used while + ;; talking to the remote shell or suchlike won't hose this + ;; variable. This approach was snarfed from ange-ftp.el. + coding-system-used + tmpfil) + ;; Write region into a tmp file. + (setq tmpfil (tramp-make-temp-file)) + ;; We say `no-message' here because we don't want the visited file + ;; modtime data to be clobbered from the temp file. We call + ;; `set-visited-file-modtime' ourselves later on. + (tramp-run-real-handler + 'write-region + (if confirm ; don't pass this arg unless defined for backward compat. + (list start end tmpfil append 'no-message lockname confirm) + (list start end tmpfil append 'no-message lockname))) + ;; Now, `last-coding-system-used' has the right value. Remember it. + (when (boundp 'last-coding-system-used) + (setq coding-system-used last-coding-system-used)) + + (tramp-smb-maybe-open-connection user host share) + (tramp-message-for-buffer + nil tramp-smb-method user host + 5 "Writing tmp file %s to file %s..." tmpfil filename) + (if (tramp-smb-send-command + user host (format "put %s \"%s\"" tmpfil file)) + (tramp-message-for-buffer + nil tramp-smb-method user host + 5 "Writing tmp file %s to file %s...done" tmpfil filename) + (error "Cannot write `%s'" filename)) + + (delete-file tmpfil) + (unless (equal curbuf (current-buffer)) + (error "Buffer has changed from `%s' to `%s'" + curbuf (current-buffer))) + (when (eq visit t) + (set-visited-file-modtime)) + ;; Make `last-coding-system-used' have the right value. + (when (boundp 'last-coding-system-used) + (setq last-coding-system-used coding-system-used)))))) + + +;; Internal file name functions + +(defun tramp-smb-get-share (path) + "Returns the share name of PATH." + (save-match-data + (when (string-match "^/?\\([^/]+\\)/" path) + (match-string 1 path)))) + +(defun tramp-smb-get-path (path convert) + "Returns the file name of PATH. +If CONVERT is non-nil exchange \"/\" by \"\\\\\"." + (save-match-data + (let ((res path)) + + (setq + res (if (string-match "^/?[^/]+/\\(.*\\)" res) + (if convert + (mapconcat + (lambda (x) (if (equal x ?/) "\\" (char-to-string x))) + (match-string 1 res) "") + (match-string 1 res)) + (if (string-match "^/?\\([^/]+\\)$" res) + (match-string 1 res) + ""))) + + ;; Sometimes we have discarded `substitute-in-file-name' + (when (string-match "\\(\\$\\$\\)\\(/\\|$\\)" res) + (setq res (replace-match "$" nil nil res 1))) + + res))) + +;; Share names of a host are cached. It is very unlikely that the +;; shares do change during connection. +(defun tramp-smb-get-file-entries (user host share path) + "Read entries which match PATH. +Either the shares are listed, or the `dir' command is executed. +Only entries matching the path are returned. +Result is a list of (PATH MODE SIZE MONTH DAY TIME YEAR)." + (save-excursion + (save-match-data + (let ((base (or (and (> (length path) 0) + (string-match "\\([^/]+\\)$" path) + (regexp-quote (match-string 1 path))) + "")) + res entry) + (set-buffer (tramp-get-buffer nil tramp-smb-method user host)) + (if (and (not share) tramp-smb-share-cache) + ;; Return cached shares + (setq res tramp-smb-share-cache) + ;; Read entries + (tramp-smb-maybe-open-connection user host share) + (when share + (tramp-smb-send-command + user host + (format "dir %s" + (if (zerop (length path)) "" (concat "\"" path "*\""))))) + (goto-char (point-min)) + ;; Loop the listing + (unless (re-search-forward tramp-smb-errors nil t) + (while (not (eobp)) + (setq entry (tramp-smb-read-file-entry share)) + (forward-line) + (when entry (add-to-list 'res entry)))) + (unless share + ;; Cache share entries + (setq tramp-smb-share-cache res))) + + + ;; Add directory itself + (add-to-list 'res '("" "dr-xr-xr-x" 0 (0 0))) + + ;; Check for matching entries + (delq nil (mapcar + (lambda (x) (and (string-match base (nth 0 x)) x)) + res)))))) + +;; Return either a share name (if SHARE is nil), or a file name +;; +;; If shares are listed, the following format is expected +;; +;; \s-\{8,8} - leading spaces +;; \S-\(.*\S-\)\s-* - share name, 14 char +;; \s- - space delimeter +;; \S-+\s-* - type, 8 char, "Disk " expected +;; \(\s-\{2,2\}.*\)? - space delimeter, comment +;; +;; Entries provided by smbclient DIR aren't fully regular. +;; They should have the format +;; +;; \s-\{2,2} - leading spaces +;; \S-\(.*\S-\)\s-* - file name, 32 chars, left bound +;; \s- - space delimeter +;; \s-*[ADHRS]* - permissions, 5 chars, right bound +;; \s- - space delimeter +;; \s-*[0-9]+ - size, 8 (Samba) or 7 (Windows) +;; chars, right bound +;; \s-\{2,2\} - space delimeter +;; \w\{3,3\} - weekday +;; \s- - space delimeter +;; [ 19][0-9] - day +;; \s- - space delimeter +;; [0-9]\{2,2\}:[0-9]\{2,2\}:[0-9]\{2,2\} - time +;; \s- - space delimeter +;; [0-9]\{4,4\} - year +;; +;; Problems: +;; * Modern regexp constructs, like spy groups and counted repetitions, aren't +;; available in older Emacsen. +;; * The length of constructs (file name, size) might exceed the default. +;; * File names might contain spaces. +;; * Permissions might be empty. +;; +;; So we try to analyze backwards. +(defun tramp-smb-read-file-entry (share) + "Parse entry in SMB output buffer. +If SHARE is result, entries are of type dir. Otherwise, shares are listed. +Result is the list (PATH MODE SIZE MTIME)." + (let ((line (buffer-substring (point) (tramp-point-at-eol))) + path mode size month day hour min sec year mtime) + + (if (not share) + + ; Read share entries + (when (string-match "^\\s-+\\(\\S-+\\)\\s-+Disk" line) + (setq path (match-string 1 line) + mode "dr-xr-xr-x" + size 0)) + + ; Real listing + (block nil + + ;; year + (if (string-match "\\([0-9]+\\)$" line) + (setq year (string-to-number (match-string 1 line)) + line (substring line 0 -5)) + (return)) + + ;; time + (if (string-match "\\([0-9]+\\):\\([0-9]+\\):\\([0-9]+\\)$" line) + (setq hour (string-to-number (match-string 1 line)) + min (string-to-number (match-string 2 line)) + sec (string-to-number (match-string 3 line)) + line (substring line 0 -9)) + (return)) + + ;; day + (if (string-match "\\([0-9]+\\)$" line) + (setq day (string-to-number (match-string 1 line)) + line (substring line 0 -3)) + (return)) + + ;; month + (if (string-match "\\(\\w+\\)$" line) + (setq month (match-string 1 line) + line (substring line 0 -4)) + (return)) + + ;; weekday + (if (string-match "\\(\\w+\\)$" line) + (setq line (substring line 0 -5)) + (return)) + + ;; size + (if (string-match "\\([0-9]+\\)$" line) + (setq size (match-string 1 line) + line (substring line 0 (- (max 8 (1+ (length size)))))) + (return)) + + ;; mode + (if (string-match "\\(\\([ADHRS]+\\)?\\s-?\\)$" line) + (setq + mode (or (match-string 2 line) "") + mode (save-match-data (format + "%s%s" + (if (string-match "D" mode) "d" "-") + (mapconcat + (lambda (x) "") " " + (concat "r" (if (string-match "R" mode) "-" "w") "x")))) + line (substring line 0 (- (1+ (length (match-string 2 line)))))) + (return)) + + ;; path + (if (string-match "^\\s-+\\(\\S-\\(.*\\S-\\)?\\)\\s-+$" line) + (setq path (match-string 1 line)) + (return)))) + + (when (and path mode size) + (setq mtime + (if (and sec min hour day month year) + (encode-time + sec min hour day + (cdr (assoc (downcase month) tramp-smb-parse-time-months)) + year) + '(0 0))) + (list path mode size mtime)))) + + +;; Connection functions + +(defun tramp-smb-send-command (user host command) + "Send the COMMAND to USER at HOST (logged into an SMB session). +Erases temporary buffer before sending the command. Returns nil if +there has been an error message from smbclient." + (save-excursion + (set-buffer (tramp-get-buffer nil tramp-smb-method user host)) + (erase-buffer) + (tramp-send-command nil tramp-smb-method user host command nil t) + (tramp-smb-wait-for-output user host))) + +(defun tramp-smb-maybe-open-connection (user host share) + "Maybe open a connection to HOST, logging in as USER, using `tramp-smb-program'. +Does not do anything if a connection is already open, but re-opens the +connection if a previous connection has died for some reason." + (let ((p (get-buffer-process + (tramp-get-buffer nil tramp-smb-method user host)))) + (save-excursion + (set-buffer (tramp-get-buffer nil tramp-smb-method user host)) + ;; Check whether it is still the same share + (unless (and p (processp p) (string-equal tramp-smb-share share)) + (when (and p (processp p)) + (delete-process p) + (setq p nil))) + ;; If too much time has passed since last command was sent, look + ;; whether process is still alive. If it isn't, kill it. + (when (and tramp-last-cmd-time + (> (tramp-time-diff (current-time) tramp-last-cmd-time) 60) + p (processp p) (memq (process-status p) '(run open))) + (unless (and p (processp p) (memq (process-status p) '(run open))) + (delete-process p) + (setq p nil)))) + (unless (and p (processp p) (memq (process-status p) '(run open))) + (when (and p (processp p)) + (delete-process p)) + (tramp-smb-open-connection user host share)))) + +(defun tramp-smb-open-connection (user host share) + "Open a connection using `tramp-smb-program'. +This starts the command `smbclient //HOST/SHARE -U USER', then waits +for a remote password prompt. It queries the user for the password, +then sends the password to the remote host. + +Domain names in USER and port numbers in HOST are acknowledged." + + (save-match-data + (let* ((buffer (tramp-get-buffer nil tramp-smb-method user host)) + (real-user user) + (real-host host) + domain port args) + + ; Check for domain ("user%domain") and port ("host#port") + (when (and user (string-match "\\(.+\\)%\\(.+\\)" user)) + (setq real-user (or (match-string 1 user) user) + domain (match-string 2 user))) + + (when (and host (string-match "\\(.+\\)#\\(.+\\)" host)) + (setq real-host (or (match-string 1 host) host) + port (match-string 2 host))) + + (if share + (setq args (list (concat "//" real-host "/" share))) + (setq args (list "-L" real-host ))) + + (if real-user + (setq args (append args (list "-U" real-user))) + (setq args (append args (list "-N")))) + + (when domain (setq args (append args (list "-W" domain)))) + (when port (setq args (append args (list "-p" port)))) + + ; OK, let's go + (tramp-pre-connection nil tramp-smb-method user host) + (tramp-message 7 "Opening connection for //%s@%s/%s..." + user host (or share "")) + + (let* ((default-directory (tramp-temporary-file-directory)) + ;; If we omit the conditional here, then we would use + ;; `undecided-dos' in some cases. With the conditional, + ;; we use nil in these cases. Which one is right? + (coding-system-for-read (unless (and (not (featurep 'xemacs)) + (> emacs-major-version 20)) + tramp-dos-coding-system)) + (p (apply #'start-process (buffer-name buffer) buffer + tramp-smb-program args))) + + (tramp-message 9 "Started process %s" (process-command p)) + (process-kill-without-query p) + (set-buffer buffer) + (set-process-sentinel + p (lambda (proc str) (setq tramp-smb-process-running nil))) + ; If no share is given, the process will terminate + (setq tramp-smb-process-running share + tramp-smb-share share) + + ; send password + (when real-user + (let ((pw-prompt "Password:")) + (tramp-message 9 "Sending password") + (tramp-enter-password p pw-prompt))) + + (unless (tramp-smb-wait-for-output user host) + (error "Cannot open connection //%s@%s/%s" + user host (or share ""))))))) + +;; We don't use timeouts. If needed, the caller shall wrap around. +(defun tramp-smb-wait-for-output (user host) + "Wait for output from smbclient command. +Sets position to begin of buffer. +Returns nil if an error message has appeared." + (save-excursion + (let ((proc (get-buffer-process (current-buffer))) + (found (progn (goto-char (point-max)) + (beginning-of-line) + (looking-at tramp-smb-prompt))) + err) + (save-match-data + ;; Algorithm: get waiting output. See if last line contains + ;; tramp-smb-prompt sentinel, or process has exited. + ;; If not, wait a bit and again get waiting output. + (while (and (not found) tramp-smb-process-running) + (accept-process-output proc) + (goto-char (point-max)) + (beginning-of-line) + (setq found (looking-at tramp-smb-prompt))) + + ;; There might be pending output. If tramp-smb-prompt sentinel + ;; hasn't been found, the process has died already. We should + ;; give it a chance. + (when (not found) (accept-process-output nil 1)) + + ;; Search for errors. + (goto-char (point-min)) + (setq err (re-search-forward tramp-smb-errors nil t))) + + ;; Add output to debug buffer if appropriate. + (when tramp-debug-buffer + (append-to-buffer + (tramp-get-debug-buffer nil tramp-smb-method user host) + (point-min) (point-max)) + (when (and (not found) tramp-smb-process-running) + (save-excursion + (set-buffer + (tramp-get-debug-buffer nil tramp-smb-method user host)) + (goto-char (point-max)) + (insert (format "[[Remote prompt `%s' not found]]\n" + tramp-smb-prompt))))) + (goto-char (point-min)) + ;; Return value is whether no error message has appeared. + (not err)))) + + +;; Snarfed code from time-date.el and parse-time.el + +(defconst tramp-smb-half-a-year '(241 17024) +"Evaluated by \"(days-to-time 183)\".") + +(defconst tramp-smb-parse-time-months '(("jan" . 1) ("feb" . 2) ("mar" . 3) + ("apr" . 4) ("may" . 5) ("jun" . 6) + ("jul" . 7) ("aug" . 8) ("sep" . 9) + ("oct" . 10) ("nov" . 11) ("dec" . 12)) +"Alist mapping month names to integers.") + +(defun tramp-smb-time-less-p (t1 t2) + "Say whether time value T1 is less than time value T2." + (unless t1 (setq t1 '(0 0))) + (unless t2 (setq t2 '(0 0))) + (or (< (car t1) (car t2)) + (and (= (car t1) (car t2)) + (< (nth 1 t1) (nth 1 t2))))) + +(defun tramp-smb-time-subtract (t1 t2) + "Subtract two time values. +Return the difference in the format of a time value." + (unless t1 (setq t1 '(0 0))) + (unless t2 (setq t2 '(0 0))) + (let ((borrow (< (cadr t1) (cadr t2)))) + (list (- (car t1) (car t2) (if borrow 1 0)) + (- (+ (if borrow 65536 0) (cadr t1)) (cadr t2))))) + + +;; `PC-do-completion' touches the returning "$$" by `substitute-in-file-name'. +;; Must be corrected. + +(defadvice PC-do-completion (around tramp-smb-advice-PC-do-completion activate) + "Changes \"$\" back to \"$$\" in minibuffer." + (if (funcall PC-completion-as-file-name-predicate) + + (progn + ;; Substitute file names + (let* ((beg (or (and (functionp 'minibuffer-prompt-end) ; Emacs 21 + (funcall 'minibuffer-prompt-end)) + (point-min))) + (end (point-max)) + (str (substitute-in-file-name (buffer-substring beg end)))) + (delete-region beg end) + (insert str) + (ad-set-arg 2 (point))) + + ;; Do `PC-do-completion' without substitution + (let* (save) + (fset 'save (symbol-function 'substitute-in-file-name)) + (fset 'substitute-in-file-name (symbol-function 'identity)) + ad-do-it + (fset 'substitute-in-file-name (symbol-function 'save))) + + ;; Expand "$" + (let* ((beg (or (and (functionp 'minibuffer-prompt-end) ; Emacs 21 + (funcall 'minibuffer-prompt-end)) + (point-min))) + (end (point-max)) + (str (buffer-substring beg end))) + (delete-region beg end) + (insert (if (string-match "\\(\\$\\)\\(/\\|$\\)" str) + (replace-match "$$" nil nil str 1) + str)))) + + ;; No file names. Behave unchanged. + ad-do-it)) + +(provide 'tramp-smb) + +;;; TODO: + +;; * Provide a local smb.conf. The default one might not be readable. +;; * Error handling in case password is wrong. +;; * Read password from "~/.netrc". +;; * Use different buffers for different shares. By this, the password +;; won't be requested again when changing shares on the same host. +;; * Return more comprehensive file permission string. Think whether it is +;; possible to implement `set-file-modes'. +;; * Handle WILDCARD and FULL-DIRECTORY-P in +;; `tramp-smb-handle-insert-directory'. +;; * Handle links (FILENAME.LNK). +;; * Maybe local tmp files should have the same extension like the original +;; files. Strange behaviour with jka-compr otherwise? +;; * Copy files in dired from SMB to another method doesn't work. +;; * Try to remove the inclusion of dummy "" directory. Seems to be at +;; several places, especially in `tramp-smb-handle-insert-directory'. +;; * Provide variables for debug. +;; * (RMS) Use unwind-protect to clean up the state so as to make the state +;; regular again. + +;;; tramp-smb.el ends here