銀月の符号

Python 使い見習いの日記・雑記

Windows にてディレクトリのアクセス日時、更新日時を変更する

Python ディレクトリの日付はどうやって変更するの? — lights on zope より。os.utime は Windows 環境ではディレクトリを操作することができないことを知りました。これは指摘されるまで気がつきませんでした。そこで対策を考えてみました。

(何か見落としているような…、標準ライブラリレベルでのサポートは無いのか。いや Win9x 系ではディレクトリのタイムスタンプを変更するなんて概念は無かった。ならば昔は Win9x 系をサポートしていた経緯から手がつけられなかった可能性はありえるかも。)

pywin32

(4/7 例外処理などを見直し)
CreateFileW, SetFileTime を使って os.utime を模した関数を作りました。file_path 引数にディレクトリを指定しても正常動作します。引数 times には os.utime 同様、(atime, mtime) という2要素のタプルを与えるようにしてください。ファイル、フォルダが見つからない、開けない、時刻が変更できないといった場合には WindowsError 例外が発生するのでこれを捕まえるようにしてください。

# coding: utf-8
import sys
import time
import win32file
import pywintypes

def utime(file_path, times=None):
    try:
        handle = win32file.CreateFileW(
                file_path,
                win32file.GENERIC_WRITE,
                0,
                None,
                win32file.OPEN_EXISTING,
                win32file.FILE_FLAG_BACKUP_SEMANTICS,
                None)
        try:
            if times is None:
                atime = mtime = time.time()
            else:
                try:
                    atime, mtime = times
                except (ValueError, TypeError), e:
                    raise \
                      TypeError(
                        u'utime() arg 2 must be a tuple (atime, mtime)'), \
                      None, \
                      sys.exc_info()[2]
            win32file.SetFileTime(
                    handle,
                    None,
                    atime,
                    mtime)
        finally:
            handle.Close()
    except pywintypes.error, e:
        raise WindowsError(e.winerror, e.strerror), None, sys.exc_info()[2]

実装解説。CreateFileW は FILE_FLAG_BACKUP_SEMANTICS を引数に与えることでディレクトリをも開くことができます。こうして得られたファイルハンドルを SetFileTime に渡して日時を変更しています。SetFileTime の第2〜第4引数は pywintypes.Time 型の CreatedTime, AccessTime, WrittenTime です。しかし、エポック秒を表す int, float や、 time.localtime などで得られる 9 要素タプル(struct_time)を渡しても動作します。pywintypes.Time 型以外が渡されたときには、まずこれに変換しようとするようです。

IronPython

IronPython ならば .NET ライブラリの FileSystemInfo を使用することで解決できます。 CPython の os.utime のような関数を作成してみました。ただし、引数 times は Unix エポック秒を表す int, float ではなく、 System.DateTime 型の 2 要素タプルです。

# coding: utf-8
import clr
import System

def utime(file_path, times=None):
    if not times:
        t = System.DateTime.Now
        times = (t, t)
        del t
    atime = times[0]
    mtime = times[1]

    if System.IO.File.Exists(file_path):
        info = System.IO.FileInfo(file_path)
    elif System.IO.Directory.Exists(file_path):
        info = System.IO.DirectoryInfo(file_path)
    else:
        raise WindowsError('%s is not found' % file_path)
    info.LastAccessTime = atime
    info.LastWriteTime = mtime

Python 3.0 で

Python 3.0.1 の os.utime はディレクトリのタイムスタンプを変更できます。なぜかできてしまいます。ドキュメントには "Whether a directory can be given for path depends on whether the operating system implements directories as files (for example, Windows does not). " って書いてあるのに。

調べると Modules/posixmodule.c の static PyObject * posix_utime(PyObject *self, PyObject *args) 関数の実装に変化が見つかりました。 CreateFileW を内部で使用しているのですが、その第6引数が Python 2.5.4 では 0 なのに対し、 Python 3.0.1 では FILE_FLAG_BACKUP_SEMANTICS になっています。

Python 2.6 で

Python 2.6.1 の os.utime もディレクトリのタイムスタンプを変更できるように修正されていることを確認しました。

Python 2.5 でもなんとか

pywin32 に頼らずに。 Python 2.5.4 の Modules/posixmodule から posix_utime 関連の箇所を抜き出して CreateFile, CreateFileW の第6引数を 0 から FILE_FLAQG_BACKUP_SEMANTICS に修正した utime.c と、それを Python 2.5 向けに VisualC++ 2008 Express Edition にてコンパイルした utime.pyd のセットです。
コンパイラVisual Studio .NET 2003 ではないので DLL の違いによる問題があるかもしれません。

#include "Python.h"
#include <windows.h>

static PyObject *
win32_error(char* function, char* filename)
{
	/* XXX We should pass the function name along in the future.
	   (_winreg.c also wants to pass the function name.)
	   This would however require an additional param to the
	   Windows error object, which is non-trivial.
	*/
	errno = GetLastError();
	if (filename)
		return PyErr_SetFromWindowsErrWithFilename(errno, filename);
	else
		return PyErr_SetFromWindowsErr(errno);
}

static PyObject *
win32_error_unicode(char* function, Py_UNICODE* filename)
{
	/* XXX - see win32_error for comments on 'function' */
	errno = GetLastError();
	if (filename)
		return PyErr_SetFromWindowsErrWithUnicodeFilename(errno, filename);
	else
		return PyErr_SetFromWindowsErr(errno);
}

static int
unicode_file_names(void)
{
	static int canusewide = -1;
	if (canusewide == -1) {
		/* As per doc for ::GetVersion(), this is the correct test for
		   the Windows NT family. */
		canusewide = (GetVersion() < 0x80000000) ? 1 : 0;
	}
	return canusewide;
}

static __int64 secs_between_epochs = 11644473600; /* Seconds between 1.1.1601 and 1.1.1970 */

static void
time_t_to_FILE_TIME(int time_in, int nsec_in, FILETIME *out_ptr)
{
	/* XXX endianness */
	__int64 out;
	out = time_in + secs_between_epochs;
	out = out * 10000000 + nsec_in / 100;
	memcpy(out_ptr, &out, sizeof(out));
}

static int
extract_time(PyObject *t, long* sec, long* usec)
{
	long intval;
	if (PyFloat_Check(t)) {
		double tval = PyFloat_AsDouble(t);
		PyObject *intobj = t->ob_type->tp_as_number->nb_int(t);
		if (!intobj)
			return -1;
		intval = PyInt_AsLong(intobj);
		Py_DECREF(intobj);
		if (intval == -1 && PyErr_Occurred())
			return -1;
		*sec = intval;
		*usec = (long)((tval - intval) * 1e6); /* can't exceed 1000000 */
		if (*usec < 0)
			/* If rounding gave us a negative number,
			   truncate.  */
			*usec = 0;
		return 0;
	}
	intval = PyInt_AsLong(t);
	if (intval == -1 && PyErr_Occurred())
		return -1;
	*sec = intval;
	*usec = 0;
        return 0;
}

PyDoc_STRVAR(posix_utime__doc__,
"utime(path, (atime, mtime))\n\
utime(path, None)\n\n\
Set the access and modified time of the file to the given values.  If the\n\
second form is used, set the access and modified times to the current time.");

static PyObject *
posix_utime(PyObject *self, PyObject *args)
{
	PyObject *arg;
	PyUnicodeObject *obwpath;
	wchar_t *wpath = NULL;
	char *apath = NULL;
	HANDLE hFile;
	long atimesec, mtimesec, ausec, musec;
	FILETIME atime, mtime;
	PyObject *result = NULL;

	if (unicode_file_names()) {
		if (PyArg_ParseTuple(args, "UO|:utime", &obwpath, &arg)) {
			wpath = PyUnicode_AS_UNICODE(obwpath);
			Py_BEGIN_ALLOW_THREADS
			hFile = CreateFileW(wpath, FILE_WRITE_ATTRIBUTES, 0,
					    NULL, OPEN_EXISTING,
					    FILE_FLAG_BACKUP_SEMANTICS, NULL);
			Py_END_ALLOW_THREADS
			if (hFile == INVALID_HANDLE_VALUE)
				return win32_error_unicode("utime", wpath);
		} else
			/* Drop the argument parsing error as narrow strings
			   are also valid. */
			PyErr_Clear();
	}
	if (!wpath) {
		if (!PyArg_ParseTuple(args, "etO:utime",
				Py_FileSystemDefaultEncoding, &apath, &arg))
			return NULL;
		Py_BEGIN_ALLOW_THREADS
		hFile = CreateFileA(apath, FILE_WRITE_ATTRIBUTES, 0,
				    NULL, OPEN_EXISTING,
				    FILE_FLAG_BACKUP_SEMANTICS, NULL);
		Py_END_ALLOW_THREADS
		if (hFile == INVALID_HANDLE_VALUE) {
			win32_error("utime", apath);
			PyMem_Free(apath);
			return NULL;
		}
		PyMem_Free(apath);
	}
	
	if (arg == Py_None) {
		SYSTEMTIME now;
		GetSystemTime(&now);
		if (!SystemTimeToFileTime(&now, &mtime) ||
		    !SystemTimeToFileTime(&now, &atime)) {
			win32_error("utime", NULL);
			goto done;
		    }
	}
	else if (!PyTuple_Check(arg) || PyTuple_Size(arg) != 2) {
		PyErr_SetString(PyExc_TypeError,
				"utime() arg 2 must be a tuple (atime, mtime)");
		goto done;
	}
	else {
		if (extract_time(PyTuple_GET_ITEM(arg, 0),
				 &atimesec, &ausec) == -1)
			goto done;
		time_t_to_FILE_TIME(atimesec, 1000*ausec, &atime);
		if (extract_time(PyTuple_GET_ITEM(arg, 1),
				 &mtimesec, &musec) == -1)
			goto done;
		time_t_to_FILE_TIME(mtimesec, 1000*musec, &mtime);
	}
	if (!SetFileTime(hFile, NULL, &atime, &mtime)) {
		/* Avoid putting the file name into the error here,
		   as that may confuse the user into believing that
		   something is wrong with the file, when it also
		   could be the time stamp that gives a problem. */
		win32_error("utime", NULL);
	}
	Py_INCREF(Py_None);
	result = Py_None;
done:
	CloseHandle(hFile);
	return result;
}

static PyMethodDef module_methods[] = {
    {"utime", posix_utime, METH_VARARGS, posix_utime__doc__},
    {NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC
initutime(void) 
{
    PyObject* m;

    m = Py_InitModule3("utime", module_methods, "utime for Python 2.5 on Win32");

    if (m == NULL)
      return;
}