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.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; }