איך נבדוק האם קובץ כבר קיים ב-S3?

כך למדתי על metadata של קבצי zip כשניסיתי לחסוך תעבורה ל-s3.

למה בכלל חשוב לי לבדוק אם קובץ קיים כבר ב-S3?

יש לי פרויקט שרץ ב-AWS עם הרבה למבדות. במהלך ה-CD של הפרויקט אני מעדכן את הלמבדות בשינויים בקוד. כדי לעדכן את הלמבדה אני מעלה את הקוד ל-S3 ואז פונה ללמבדה עם הגרסה החדשה של הקובץ מ-S3. אם אני יודע שהקוד לא השתנה, אני יכול לחסוך כמה דקות של עדכון כל הלמבדות שלי ובנוסף חוסך תעבורה ל-S3 שעולה (ממש מעט) כסף .

מנגנון ה-ETag

לכל אובייקט ב-S3 יש ETag שהוא מזהה ייחודי של תוכן האובייקט. מנגנון היצירה של ה-ETag תלוי בצורה שבה הועלה הקובץ ל-S3. אם מעלים דרך ה-sdk עם הגדרות ברירת המחדל, ה-ETag יחושב על ידי MD5. אם הקובץ גדול מ-16 MB, לכל חתיכה של 16 MB מחושב ואז שוב מחושב על כל החתיכות ביחד, עם מספר נוסף שמסמן את מספר החתיכות.

1const chunk = 1024 * 1024 * 16; // 16MB
2
3const md5 = (data: Buffer) => crypto.createHash('md5').update(data).digest('hex');
4
5async function getEtagOfFile(stream: Buffer) {
6  if (stream.length <= chunk) {
7    return md5(stream);
8  }
9  const md5Chunks: string[] = [];
10  const chunksNumber = Math.ceil(stream.length / chunk);
11  for (let i = 0; i < chunksNumber; i += 1) {
12    const chunkStream = Uint8Array.prototype.slice.call(stream, i * chunk, (i + 1) * chunk);
13    md5Chunks.push(md5(chunkStream));
14  }
15
16  return `${md5(Buffer.from(md5Chunks.join(''), 'hex'))}-${chunksNumber}`;
17}
18

איך אני יודע? כתבתי את זה באינטרנט מזמן אז בטוח זה נכון.

אז הכל טוב?

הקוד למעלה עבד מצוין על המחשב שלי, אבל כשהרצתי אותו ב-Github Actions הוא נתן תוצאות שונות בכל פעם בכל פעם שבדקתי את ה-hash של קובץ ה-zip קיבלתי תוצאה שונה, למרות שהקוד נשאר זהה לחלוטין. כשניסיתי לשחזר את הבעיה מקומית, גיליתי (בעזרתו האדיבה של ChatGPT) שהבעיה נובעת מכך שתיקיית ה-dist נוצרת מחדש בכל build, וכך משתנים נתוני ה-metadata של הקבצים. אז גם אם הקוד זהה לחלוטין בקובץ ה-zip יש metadata של כל הקבצים שכולל את תאריך היצירה שלהם. מכיוון שבכל build תיקיית ה-dist נוצרת מחדש, אז יש בעייה שקובץ ה-zip יהיה תמיד שונה.

כדי לראות את את ה-metadata אפשר ליצור קובץ zip ואז לראות את ה-metadata באמצעות unzip -l

1zip -rq9 target.zip target_directory
2unzip -l target.zip > metadata.txt
3

אם ניצור מחדש את target_directory, אפילו אם הקוד יהיה זהה לחלוטין, ה-metadata ישתנה. בעיה נוספת שיכולה לצוץ היא סדר הקבצים בתוך ה-zip. אפילו אם הקוד זהה לחלוטין אבל משום מה ב-zip הם מסודרים שונה בכל פעם, ה-hash ישתנה בין הריצות/ אז מה עושים?

כך ניצור קובץ zip עם metadata זהה לחלוטין בכל ריצה

1# קודם נעבור על כל הקבצים ונקבע שתאריך היצירה שלהם הוא תאריך שרירותי כלשהוא
2find target_directory -exec touch -t 202401010000 {} + && \
3# במקום שם התיקיה אני משתמש בפקודה שממיינת את תוכן התיקיה בסדר אלפאנומרי, כך הסדר של הקבצים יישאר זהה תמיד
4zip -rq9 target.zip $(find target_directory -type f | sort)
5

בצורה הזו, כל עוד הקוד שלנו נשאר זהה, ה-metadata של ה-zip לא יפריע לנו. בצורה הזו אנחנו חוסכים זמן, עלויות קטנות, ובעיקר מבטיחים שה-build שלנו יהיה צפוי ויציב