مقال تقني

حفظ آمن من الأعطال في HotXLS: ملفات مؤقتة مرحلية في Delphi

عملية حفظ تموت في منتصف الطريق، سواء من إعادة تشغيل قسرية، أو عملية أُنهيت، أو قرص امتلأ في منتصف الكتابة، كانت تعني تقليديًا شيئًا واحدًا لصيغة مبنية حول الكتابة في المكان: أيًّا كانت البايتات التي وصلت القرص قبل المقاطعة هي ما تسترجعه، ومصنّف عمل مبتور لا يُفتح مرة أخرى. يغلق HotXLS نمط الفشل هذا بمسار حفظ آمن من الأعطال يُستخدم لكل ملف XLSX وODS وXLS كلاسيكي يكتبه. يكتب كل استدعاء SaveAs الملف الجديد الكامل إلى ملف مؤقت يُنشأ بجوار الوجهة، ثم يُثبته بإعادة تسمية ذرية واحدة عبر MoveFileExW من واجهة برمجة ويندوز، بحيث لا يمكن لحفظ مقاطَع إلا أن يفشل في إنتاج الملف الجديد، ولا يتلف أبدًا الملف الذي كان لديك بالفعل. يعمل نظام المرحلة-ثم-التبديل نفسه بشكل موحّد عبر محركي الحفظ في HotXLS، كاتب BIFF8 خلف XLS الكلاسيكي وكاتب OOXML خلف XLSX وODS، وهو نمط يستحق الاقتباس لأي ملف تكتب عليه شيفرة Delphi الخاصة بك مباشرة، جداول بيانات أم لا

ماذا يحدث إذا انقطع حفظ مصنّف عمل في المنتصف؟

الإجابة المباشرة أن الأمر يعتمد كليًا على كيفية لمس الكاتب لملف الوجهة، والتنفيذ الشائع، فتح الملف الهدف وبث المحتوى الجديد مباشرة فيه، جيد طالما لم يحدث خطأ أبدًا. بمجرد أن يحدث خطأ، عطل، إنهاء قسري لعملية، مشاركة شبكية تنقطع في منتصف الكتابة، يُترك الملف على القرص في أيًّا كانت الحالة الوسيطة التي وصل إليها الكاتب: دليل ZIP المركزي الذي لم يُلحق أبدًا لـXLSX أو ODS، أو تدفق BIFF يفتقد سجلات يتوقعها قارئ لـXLS الكلاسيكي. لا يصلح Excel ذلك بسلاسة، ولا أي مستهلك آخر يتوقع ملفًا كاملًا، لذا فإن النتيجة العملية مصنّف عمل فُتح جيدًا بالأمس ويرفض الفتح اليوم

كيف يُرحّل HotXLS كل عملية حفظ خلف تبديل ذري واحد

لا يفتح HotXLS أبدًا ملف الوجهة للكتابة مباشرة، لأي من الصيغ الثلاث التي يحفظها. التسلسل بالشكل نفسه في كل مرة: ابنِ المخرجات الكاملة في مكان ليس الملف الذي لدى المستخدم بالفعل على القرص، ولا تنقله إلى مكانه إلا بمجرد أن ينجح ذلك البناء تمامًا. بشكل ملموس، تُنشئ SaveAs ملفًا مؤقتًا فارغًا في المجلد نفسه الذي فيه المسار الهدف، وتكتب مصنّف العمل الجديد بأكمله في ذلك الملف المؤقت، ولا تُثبت الملف المؤقت فوق الوجهة بإعادة تسمية واحدة إلا بعد أن تعود تلك الكتابة دون خطأ. لا شيء من هذا يتطلب خاصية للتفعيل؛ إنه ببساطة ما تفعله SaveAs لمسار ملف عادي، في كل استدعاء

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Report');
    Sheet.Cells[1, 1].Value := 'Nothing special to enable here';
    // If this call is interrupted, monthly-report.xlsx on disk stays
    // either the old version, complete, or the new version, complete
    if Book.SaveAs('monthly-report.xlsx', xlsxOpenXMLWorkbook) <> 1 then
      raise Exception.Create('Save failed, see Book.LastDiagnostic');
  finally
    Book.Free;
  end;
end;

يُطبَّق النظام نفسه على كاتب XLS الكلاسيكي، لا كاتب OOXML وحده، بل يتشارك الملفان المؤقتان اصطلاح تسمية أيضًا: يستدعي كلاهما واجهة GetTempFileNameW الخاصة بويندوز بالبادئة hxl، بحيث يمكن لحفظ انقطع قبل التنظيف أن يترك خلفه ملفًا شاردًا باسم مثل hxl4C2A.tmp يجلس بجوار مصنّف العمل الخاص بك. ذلك الملف ليس تلفًا، إنه دليل على أن الآلية عملت تمامًا كما صُمِّمت: توقفت الكتابة غير المكتملة هناك، ومصنّف العمل الفعلي الخاص بك لم يُفتح للكتابة أصلًا. رؤية واحد بعد عطل آمنة الحذف ولا شيء للتحقيق فيه

لماذا يُرحّل الملف المؤقت بجوار مصنّف العمل بدلًا من %TEMP%؟

الإجابة القصيرة أن إعادة تسمية MoveFileExW ذرية فقط عندما يقع المصدر والوجهة على وحدة التخزين نفسها، والطريقة الأضمن لضمان ذلك دون مطالبة المستدعي بإعداد أي شيء هي اشتقاق موضع الملف المؤقت من مسار الوجهة نفسه. يحسب HotXLS مجلد الهدف نفسه ويسلّم ذلك الدليل مباشرة إلى GetTempFileNameW، بحيث يُنشأ الملف المؤقت دائمًا على المحرك نفسه، الوحدة نفسها، الخاصة بالملف الذي يوشك على استبداله، تلقائيًا، لكل حفظ. لو كانت المكتبة قد رحّلت الكتابات بدلًا من ذلك في مجلد المؤقت النظامي، لكان مسار هدف على محرك مختلف أو وحدة شبكية مربوطة قد حوّل الخطوة النهائية إلى عملية عبر وحدات تخزين، وهو ما ترفضه واجهة ويندوز البرمجية كليًا، أو، إذا اختار المستدعي صراحة علمًا إضافيًا لا يضبطه HotXLS هنا، تتدهور بصمت إلى نسخ غير ذري متبوع بحذف، معيدةً فتح نافذة المقاطعة نفسها التي وُجدت هذه الآلية بأكملها لإغلاقها

خطوة التثبيت: MoveFileExW، والكتابة المباشرة، وما يحدث عند الفشل

الخطوة الأخيرة لكل حفظ هي استدعاء واحد بالضبط لواجهة ويندوز البرمجية، MoveFileExW، يحمل علمين يؤدي كل منهما عملًا مختلفًا. MOVEFILE_REPLACE_EXISTING هو ما يسمح لإعادة التسمية بالوقوع على ملف موجود بالفعل؛ بدونه، تفشل إعادة تسمية تستهدف مسارًا موجودًا ببساطة، ما كان سيبطل الغرض بأكمله من حفظ يُقصد به استبدال مصنّف عمل لديك بالفعل. MOVEFILE_WRITE_THROUGH تغطي الديمومة: تخبر الدالة ألا تعود حتى يكتمل النقل فعليًا على القرص، بدلًا من العودة بمجرد أن تُوضع إعادة التسمية في قائمة انتظار فقط، مغلقة سباقًا أضيق لكنه حقيقي حيث يمكن لعطل فور عودة SaveAs أن يلتقط التبديل لا يزال جاريًا. إذا تعذّر إنشاء الملف المؤقت، أو فشلت إعادة التسمية النهائية لأي سبب (مشكلة إذن، وجهة مقفلة، عدم تطابق وحدة تخزين)، يحذف HotXLS الملف المؤقت بنفسه بدلًا من ترك بقايا، ويُترك ملف الوجهة تمامًا كما كان قبل الاستدعاء

Result := Book.SaveAs(TargetPath, xlsxOpenXMLWorkbook);
if Result <> 1 then
begin
  // TargetPath on disk is unchanged; safe to retry, alert, or
  // fall back to a different path without touching prior output
  LogWriter.Write(Format('SaveAs failed (%d): %s',
    [Book.LastDiagnostic.Code, Book.LastDiagnostic.Message]));
  Exit(False);
end;

تحتفظ SaveAs نفسها باصطلاح الإرجاع المشترك عبر HotXLS، واحد عند النجاح، رقم سالب عند الفشل، لكن عددًا صحيحًا مجردًا لا يقول لماذا فشل الحفظ، ومعاملة كل نتيجة سالبة بالطريقة نفسها يُهدر معلومة يمكن لسياسة إعادة المحاولة استخدامها فعليًا. تحمل خاصية LastDiagnostic، ومجموعة Diagnostics الأكمل خلفها، الرسالة التي ولّدها HotXLS داخليًا، مميّزةً ملفًا مؤقتًا تعذّر إنشاؤه عن إعادة تسمية رفضها ويندوز. مهمة دفعية تسجّل Code وMessage عند كل فشل SaveAs تبني بالضبط الدليل الذي تريده في المرة الوحيدة التي يبلّغ فيها عميل عن حفظ لم يفعل شيئًا بصمت

XLS الكلاسيكي يدفع بالذاكرة، وXLSX وODS يدفعان بالقرص

يصل محركا الحفظ إلى النتيجة الآمنة من الأعطال نفسها عبر طريقين مختلفين، والفرق يهم إذا كنت بالفعل تضبط أيًّا منهما لمهمة دفعية كبيرة. يبني كاتب XLS الكلاسيكي مستند OLE المركّب بأكمله في الذاكرة أولًا، مستخدمًا تخزينًا هيكليًا مدعومًا بمقبض ذاكرة، ولا ينسخ ذلك المخزن المؤقت المنتهي إلى الملف المؤقت الشقيق إلا في كتابة واحدة؛ المنطق في مصدر HotXLS نفسه مباشر: بناء الملف بأكمله في الذاكرة أولًا هو ما يمنع حفظًا فاشلًا أو ملغى من بتر الوجهة أبدًا. أما كاتب XLSX وODS فيبث بدلًا من ذلك مدخلات ZIP الخاصة به إلى الملف المؤقت أثناء إنتاجها، الترحيل نفسه على مستوى الملف بملف تعريف ذاكرة مختلف. إذا كنت بالفعل تعتمد على StreamingWrite لإبقاء تصديرات XLSX الكبيرة داخل حد ذاكرة حاوية، اعلم أن الرافعة المكافئة لتصدير XLS الكلاسيكي غير موجودة بالشكل نفسه: الضمان الآمن من الأعطال غير مشروط في كلتا الحالتين، لكن تصدير .xls قديم كبير جدًا يحمل مخرجاته الكاملة في الذاكرة العشوائية بصرف النظر، وهي مقايضة مشروحة بتفصيل أكبر في مقالتنا حول الكتابة المتدفقة لمهام الدُفعات على الخادم

تطبيق النمط نفسه خارج HotXLS، وأين ينتهي الضمان

اقتباس النمط هو في الغالب مسألة توصيل استدعائي واجهة ويندوز البرمجية نفسيهما اللذين يعتمد عليهما HotXLS داخليًا. تمنحك GetTempFileNameW ملفًا فارغًا بمعرّف فريد في مجلد تختاره، وتُثبّت MoveFileExW كتابتك المنتهية فوق الوجهة الحقيقية في خطوة واحدة؛ نسخة أدنى من الروتين نفسه الذي يشغّله HotXLS قبل كل SaveAs تبدو هكذا

function SaveFileAtomically(const Path: WideString; const Contents: TBytes): Boolean;
var
  Dir, TempName: WideString;
  Buffer: array[0..MAX_PATH] of WideChar;
  FS: TFileStream;
begin
  Result := False;
  Dir := ExtractFilePath(ExpandFileName(Path));
  FillChar(Buffer, SizeOf(Buffer), 0);
  if GetTempFileNameW(PWideChar(Dir), 'app', 0, @Buffer[0]) = 0 then
    Exit;
  TempName := PWideChar(@Buffer[0]);
  try
    FS := TFileStream.Create(TempName, fmCreate or fmShareExclusive);
    try
      FS.WriteBuffer(Contents[0], Length(Contents));
    finally
      FS.Free;
    end;
    Result := MoveFileExW(PWideChar(TempName), PWideChar(ExpandFileName(Path)),
      MOVEFILE_REPLACE_EXISTING or MOVEFILE_WRITE_THROUGH);
  finally
    if not Result then
      DeleteFileW(PWideChar(TempName));
  end;
end;

للضمان حدود حقيقية تستحق المعرفة قبل الاعتماد عليه أعمى. ترحيل نسخة كاملة قبل استبدال الأصل يعني أن الحفظ يحتاج لبرهة مساحة قرص لكل من الملف القديم والجديد، ضعف حجم مصنّف العمل تقريبًا طوال مدة الكتابة، وهو أمر جيد لتقرير ويستحق التحقق منه لتصدير بحجم عدة غيغابايت يعمل على وحدة تخزين شبه ممتلئة. يجب أيضًا أن يقع الملف المؤقت في المجلد نفسه الذي فيه الوجهة، لذا فإن أيًّا كان الحساب الذي يعمل HotXLS تحته يحتاج إذن إنشاء ملف على ذلك المجلد تحديدًا، لا مجرد إذن الكتابة فوق الملف الواحد الذي يعرفه بالفعل؛ نشر يقفل مجلد وجهة على تعديلات في المكان لأسماء ملفات موجودة محددة، بدلًا من وصول كتابة على مستوى المجلد، سيرى SaveAs يفشل عند خطوة الملف المؤقت رغم أن الكتابة المباشرة المكافئة كانت ستنجح

حدّان آخران يستحقان الإشارة إليهما بوضوح. وجهة على مشاركة شبكية أو داخل مجلد يزامنه OneDrive أو عميل مشابه يمكن أن تتصرف بشكل مختلف عن NTFS المحلي رغم أن ويندوز لا يزال يبلّغ عنها كوحدة تخزين واحدة، بما أن برنامج تشغيل نظام الملفات أمامها قد لا ينفّذ إعادة التسمية بالطريقة نفسها؛ إذا كان هدف النشر الخاص بك يحفظ عبر مسار شبكي، يستحق الأمر اختبار مقاطعة قسرية هناك تحديدًا بدلًا من افتراض أن سلوك القرص المحلي ينتقل كما هو. والآلية بأكملها محصورة في الحفظ إلى ملف مسمّى. استدعِ SaveAs مقابل TStream بدلًا من ذلك، وسيكتب HotXLS في أيًّا كان التدفق الذي سلّمته له مباشرة، دون ملف وجهة يجب ترحيله أو حمايته، لأن ديمومة ذلك التدفق (مخزن ذاكرة مؤقت، رفع شبكي، كائن ثنائي في قاعدة بيانات) مسؤولية شيفرتك بالكامل من تلك النقطة فصاعدًا

تمريرة تحقق تعتمد بالضبط على هذا الضمان لاحقًا، بما في ذلك النوع المدمج في منصة تدقيق وتحويل مصنّفات العمل: ملف يُعاد فتحه ويعود ناقصًا أو مفقودًا مشكلة تحويل حقيقية يجب تتبعها، لا حفظًا انقطع في المنتصف وترك شيئًا غامضًا على القرص. الكتابات المرحلية الآمنة من الأعطال مدمجة في SaveAs لكل مصنّف عمل XLSX وODS وXLS كلاسيكي ينتجه مكوّن HotXLS لـDelphi وC++Builder، دون أي إعداد مطلوب لتفعيلها