# -*- coding: iso-8859-1 -*-
"""Provide a wrapper around common filesystem operations, including
iterating over the files in a directory structure and copying,
moving and linking them. Wherever possible iterators are used,
making it possible to act on large directory structures without
loading them all into memory.
"""
from __future__ import with_statement
import os, sys
import codecs
import collections
import contextlib
import filecmp
import fnmatch
import msvcrt
import operator
import re
import struct
import threading
import time
import zipfile

import ntsecuritycon
import pywintypes
import winerror
import win32api
import win32con
import win32event
import win32file
import winioctlcon

if not hasattr (winerror, 'ERROR_BAD_RECOVERY_POLICY'):
  winerror.ERROR_BAD_RECOVERY_POLICY = 6012

from winsys import constants, core, exc, handles, security, utils, _kernel32

sep = unicode (os.sep)
seps = u"/\\"

class x_fs (exc.x_winsys):
  u"Base for all fs-related exceptions"

class x_no_such_file (x_fs):
  u"Raised when a file could not be found"

class x_too_many_files (x_fs):
  u"Raised when more than one file matches a name"

class x_invalid_name (x_fs):
  u"Raised when a filename contains invalid characters"

class x_no_certificate (x_fs):
  u"Raised when encryption is attempted without a certificate"

class x_not_ready (x_fs):
  u"Raised when a device is not ready"

class x_sharing_violation (x_fs):
  u"Raised when a sharing violation occurs"

WINERROR_MAP = {
  winerror.ERROR_ACCESS_DENIED : exc.x_access_denied,
  winerror.ERROR_PATH_NOT_FOUND : x_no_such_file,
  winerror.ERROR_FILE_NOT_FOUND : x_no_such_file,
  winerror.ERROR_BAD_NETPATH : x_no_such_file,
  winerror.ERROR_INVALID_NAME : x_invalid_name,
  winerror.ERROR_BAD_RECOVERY_POLICY : x_no_certificate,
  winerror.ERROR_NOT_READY : x_not_ready,
  winerror.ERROR_INVALID_HANDLE : exc.x_invalid_handle,
  winerror.ERROR_SHARING_VIOLATION : x_sharing_violation,
}
wrapped = exc.wrapper (WINERROR_MAP, x_fs)

def ignore_access_errors (exc_info):
  return exc_info[0] is exc.x_access_denied

PyHANDLE = pywintypes.HANDLEType

FILE_ACCESS = constants.Constants.from_pattern ("FILE_*", namespace=ntsecuritycon)
FILE_ACCESS.update (constants.STANDARD_ACCESS)
FILE_ACCESS.update (constants.GENERIC_ACCESS)
FILE_ACCESS.update (constants.ACCESS)
FILE_ACCESS.doc ("File-specific access rights")

FILE_SHARE = constants.Constants.from_pattern (u"FILE_SHARE_*", namespace=win32file)
FILE_SHARE.doc (u"Ways of sharing a file for reading, writing, &c.")

FILE_CREATION = constants.Constants.from_list ([
  u"CREATE_ALWAYS",
  u"CREATE_NEW",
  u"OPEN_ALWAYS",
  u"OPEN_EXISTING",
  u"TRUNCATE_EXISTING"
], namespace=win32con)
FILE_CREATION.doc (u"Options when creating a file")

FILE_FLAG = constants.Constants.from_pattern (u"FILE_FLAG_*", namespace=win32con)
FILE_FLAG.doc (u"File flags")

FILE_NOTIFY_CHANGE = constants.Constants.from_pattern (u"FILE_NOTIFY_CHANGE_*", namespace=win32con)
FILE_NOTIFY_CHANGE.doc (u"Notification types to watch for when a file changes")
FILE_ACTION = constants.Constants.from_dict (dict (
  ADDED = 1,
  REMOVED = 2,
  MODIFIED = 3,
  RENAMED_OLD_NAME = 4,
  RENAMED_NEW_NAME = 5
))
FILE_ACTION.doc (u"Results of a file change")
FILE_ATTRIBUTE = constants.Constants.from_pattern (u"FILE_ATTRIBUTE_*", namespace=win32file)
FILE_ATTRIBUTE.update (dict (
  ENCRYPTED=0x00004000,
  REPARSE_POINT=0x00000400,
  SPARSE_FILE=0x00000200,
  VIRTUAL=0x00010000,
  NOT_CONTENT_INDEXES=0x00002000,
))
FILE_ATTRIBUTE.doc (u"Attributes applying to any file")
PROGRESS = constants.Constants.from_pattern (u"PROGRESS_*", namespace=win32file)
PROGRESS.doc (u"States within a file move/copy progress")
MOVEFILE = constants.Constants.from_pattern (u"MOVEFILE_*", namespace=win32file)
MOVEFILE.doc (u"Options when moving a file")
VOLUME_FLAG = constants.Constants.from_dict (dict (
  FILE_CASE_SENSITIVE_SEARCH      = 0x00000001,
  FILE_CASE_PRESERVED_NAMES       = 0x00000002,
  FILE_UNICODE_ON_DISK            = 0x00000004,
  FILE_PERSISTENT_ACLS            = 0x00000008,
  FILE_FILE_COMPRESSION           = 0x00000010,
  FILE_VOLUME_QUOTAS              = 0x00000020,
  FILE_SUPPORTS_SPARSE_FILES      = 0x00000040,
  FILE_SUPPORTS_REPARSE_POINTS    = 0x00000080,
  FILE_SUPPORTS_REMOTE_STORAGE    = 0x00000100,
  FILE_VOLUME_IS_COMPRESSED       = 0x00008000,
  FILE_SUPPORTS_OBJECT_IDS        = 0x00010000,
  FILE_SUPPORTS_ENCRYPTION        = 0x00020000,
  FILE_NAMED_STREAMS              = 0x00040000,
  FILE_READ_ONLY_VOLUME           = 0x00080000,
  FILE_SEQUENTIAL_WRITE_ONCE      = 0x00100000,
  FILE_SUPPORTS_TRANSACTIONS      = 0x00200000
), pattern=u"FILE_*")
VOLUME_FLAG.doc (u"Characteristics of a volume")
DRIVE_TYPE = constants.Constants.from_pattern (u"DRIVE_*", namespace=win32file)
DRIVE_TYPE.doc (u"Types of drive")
COMPRESSION_FORMAT = constants.Constants.from_dict (dict (
  NONE = 0x0000,
  DEFAULT = 0x0001,
  LZNT1 = 0x0002
))
COMPRESSION_FORMAT.doc (u"Ways in which a file can be compressed")
FSCTL = constants.Constants.from_pattern (u"FSCTL_*", namespace=winioctlcon)
FSCTL.doc (u"Types of fsctl operation")

PyHANDLE = pywintypes.HANDLEType

LEGAL_FILECHAR = r"[^\?\*\\\:\/]"
LEGAL_FILECHARS = LEGAL_FILECHAR + "+"
LEGAL_VOLCHAR = r"[^\?\*\\\/]"
LEGAL_VOLCHARS = LEGAL_VOLCHAR + "+"
UNC = sep * 4 + LEGAL_FILECHARS + sep * 2 + LEGAL_FILECHARS
HEXDIGIT = "[0-9a-f]"
DRIVE = r"[A-Za-z]:"
VOLUME_PREAMBLE = sep * 4 + r"\?" + sep * 2
VOLUME = VOLUME_PREAMBLE + "Volume\{%s{8}-%s{4}-%s{4}-%s{4}-%s{12}\}" % (HEXDIGIT, HEXDIGIT, HEXDIGIT, HEXDIGIT, HEXDIGIT)
DRIVE_VOLUME = VOLUME_PREAMBLE + DRIVE
PREFIX = r"((?:%s|%s|%s|%s)\\?)" % (UNC, DRIVE, VOLUME, DRIVE_VOLUME)
PATHSEG = "(" + LEGAL_FILECHARS + ")" + sep * 2 + "?"
PATHSEGS = "(?:%s)*" % PATHSEG
FILEPATH = PREFIX + PATHSEGS

def get_parts (filepath):
  ur"""Helper function to regularise a file path and then
  to pick out its drive and path components.

  Attempt to match the first part of the string against
  known path leaders::

    <drive>:\
    \\?\<drive>:\
    \\?\Volume{xxxx-...}\
    \\server\share\

  If that fails, assume the path is relative.

  ============================= ======================================
  Path                          Parts
  ============================= ======================================
  c:/                           ["c:\\", ""]
  c:/t                          ["c:\\", "t"]
  c:/t/                         ["c:\\", "t"]
  c:/t/test.txt                 ["c:\\", "t", "test.txt"]
  c:/t/s/test.txt               ["c:\\", "t", "s", "test.txt"]
  c:test.txt                    ["c:", "test.txt"]
  s/test.txt                    ["", "s", "test.txt"]
  \\\\server\\share             ["\\\\server\\share\\", ""]
  \\\\server\\share\\a.txt      ["\\\\server\\share\\", "a.txt"]
  \\\\server\\share\\t\\a.txt   ["\\\\server\\share\\", "t", "a.txt"]
  \\\\?\\c:\test.txt            ["\\\\?\\c:\\", "test.txt"]
  \\\\?\\Volume{xxxx-..}\\t.txt ["\\\\?\Volume{xxxx-..}\\", "t.txt"]
  ============================= ======================================

  The upshot is that the first item i
n the list returned is
  always the root, one of: a 
slash-terminated drive/volume/share
  for an absolute path; an empty string for a relative path; or
  a drive-colon for a drive-relative path.

  All other items before the last one represent the directories along
  the way. The last item is the filename or directory name.

  The original filepath can usually be reconstructed as::

    from winsys import fs
    filepath = "c:/temp/abc.txt"
    parts = fs.get_parts (filepath)
    assert parts[0] + "\\".join (parts[1:]) == filepath

  The exception is when a root (UNC or volume) is given without
  a trailing slash. This is added in. Or if a directory is given
  with a trailing slash. This is stripped off.
  """
  filepath = filepath.replace ("/", sep)
  prefix_match = re.match (PREFIX, filepath)

  if prefix_match:
    prefix = prefix_match.group (